porthole 0.99.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +415 -0
- data/Rakefile +89 -0
- data/bin/porthole +94 -0
- data/lib/porthole.rb +304 -0
- data/lib/porthole/context.rb +142 -0
- data/lib/porthole/generator.rb +195 -0
- data/lib/porthole/parser.rb +263 -0
- data/lib/porthole/settings.rb +226 -0
- data/lib/porthole/sinatra.rb +205 -0
- data/lib/porthole/template.rb +58 -0
- data/lib/porthole/version.rb +3 -0
- data/lib/rack/bug/panels/mustache_panel.rb +81 -0
- data/lib/rack/bug/panels/mustache_panel/mustache_extension.rb +27 -0
- data/lib/rack/bug/panels/mustache_panel/view.mustache +46 -0
- data/man/porthole.1 +165 -0
- data/man/porthole.1.html +213 -0
- data/man/porthole.1.ron +127 -0
- data/man/porthole.5 +539 -0
- data/man/porthole.5.html +422 -0
- data/man/porthole.5.ron +324 -0
- data/test/autoloading_test.rb +56 -0
- data/test/fixtures/comments.porthole +1 -0
- data/test/fixtures/comments.rb +14 -0
- data/test/fixtures/complex_view.porthole +17 -0
- data/test/fixtures/complex_view.rb +34 -0
- data/test/fixtures/crazy_recursive.porthole +9 -0
- data/test/fixtures/crazy_recursive.rb +31 -0
- data/test/fixtures/delimiters.porthole +8 -0
- data/test/fixtures/delimiters.rb +23 -0
- data/test/fixtures/dot_notation.porthole +10 -0
- data/test/fixtures/dot_notation.rb +25 -0
- data/test/fixtures/double_section.porthole +7 -0
- data/test/fixtures/double_section.rb +14 -0
- data/test/fixtures/escaped.porthole +1 -0
- data/test/fixtures/escaped.rb +14 -0
- data/test/fixtures/inner_partial.porthole +1 -0
- data/test/fixtures/inner_partial.txt +1 -0
- data/test/fixtures/inverted_section.porthole +7 -0
- data/test/fixtures/inverted_section.rb +14 -0
- data/test/fixtures/lambda.porthole +7 -0
- data/test/fixtures/lambda.rb +31 -0
- data/test/fixtures/method_missing.rb +19 -0
- data/test/fixtures/namespaced.porthole +1 -0
- data/test/fixtures/namespaced.rb +25 -0
- data/test/fixtures/nested_objects.porthole +17 -0
- data/test/fixtures/nested_objects.rb +35 -0
- data/test/fixtures/node.porthole +8 -0
- data/test/fixtures/partial_with_module.porthole +4 -0
- data/test/fixtures/partial_with_module.rb +37 -0
- data/test/fixtures/passenger.conf +5 -0
- data/test/fixtures/passenger.rb +27 -0
- data/test/fixtures/recursive.porthole +4 -0
- data/test/fixtures/recursive.rb +14 -0
- data/test/fixtures/simple.porthole +5 -0
- data/test/fixtures/simple.rb +26 -0
- data/test/fixtures/template_partial.porthole +2 -0
- data/test/fixtures/template_partial.rb +18 -0
- data/test/fixtures/template_partial.txt +4 -0
- data/test/fixtures/unescaped.porthole +1 -0
- data/test/fixtures/unescaped.rb +14 -0
- data/test/fixtures/utf8.porthole +3 -0
- data/test/fixtures/utf8_partial.porthole +1 -0
- data/test/helper.rb +7 -0
- data/test/parser_test.rb +78 -0
- data/test/partial_test.rb +168 -0
- data/test/porthole_test.rb +677 -0
- data/test/spec_test.rb +68 -0
- data/test/template_test.rb +20 -0
- metadata +127 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
class Porthole
|
2
|
+
# The Generator is in charge of taking an array of Porthole tokens,
|
3
|
+
# usually assembled by the Parser, and generating an interpolatable
|
4
|
+
# Ruby string. This string is considered the "compiled" template
|
5
|
+
# because at that point we're relying on Ruby to do the parsing and
|
6
|
+
# run our code.
|
7
|
+
#
|
8
|
+
# For example, let's take this template:
|
9
|
+
#
|
10
|
+
# Hi {{thing}}!
|
11
|
+
#
|
12
|
+
# If we run this through the Parser we'll get these tokens:
|
13
|
+
#
|
14
|
+
# [:multi,
|
15
|
+
# [:static, "Hi "],
|
16
|
+
# [:porthole, :etag, "thing"],
|
17
|
+
# [:static, "!\n"]]
|
18
|
+
#
|
19
|
+
# Now let's hand that to the Generator:
|
20
|
+
#
|
21
|
+
# >> puts Porthole::Generator.new.compile(tokens)
|
22
|
+
# "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
|
23
|
+
#
|
24
|
+
# You can see the generated Ruby string for any template with the
|
25
|
+
# porthole(1) command line tool:
|
26
|
+
#
|
27
|
+
# $ porthole --compile test.porthole
|
28
|
+
# "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
|
29
|
+
class Generator
|
30
|
+
# Options are unused for now but may become useful in the future.
|
31
|
+
def initialize(options = {})
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given an array of tokens, returns an interpolatable Ruby string.
|
36
|
+
def compile(exp)
|
37
|
+
"\"#{compile!(exp)}\""
|
38
|
+
end
|
39
|
+
|
40
|
+
# Given an array of tokens, converts them into Ruby code. In
|
41
|
+
# particular there are three types of expressions we are concerned
|
42
|
+
# with:
|
43
|
+
#
|
44
|
+
# :multi
|
45
|
+
# Mixed bag of :static, :porthole, and whatever.
|
46
|
+
#
|
47
|
+
# :static
|
48
|
+
# Normal HTML, the stuff outside of {{portholes}}.
|
49
|
+
#
|
50
|
+
# :porthole
|
51
|
+
# Any Porthole tag, from sections to partials.
|
52
|
+
#
|
53
|
+
# To give you an idea of what you'll be dealing with take this
|
54
|
+
# template:
|
55
|
+
#
|
56
|
+
# Hello {{name}}
|
57
|
+
# You have just won ${{value}}!
|
58
|
+
# {{#in_ca}}
|
59
|
+
# Well, ${{taxed_value}}, after taxes.
|
60
|
+
# {{/in_ca}}
|
61
|
+
#
|
62
|
+
# If we run this through the Parser, we'll get back this array of
|
63
|
+
# tokens:
|
64
|
+
#
|
65
|
+
# [:multi,
|
66
|
+
# [:static, "Hello "],
|
67
|
+
# [:porthole, :etag, "name"],
|
68
|
+
# [:static, "\nYou have just won $"],
|
69
|
+
# [:porthole, :etag, "value"],
|
70
|
+
# [:static, "!\n"],
|
71
|
+
# [:porthole,
|
72
|
+
# :section,
|
73
|
+
# "in_ca",
|
74
|
+
# [:multi,
|
75
|
+
# [:static, "Well, $"],
|
76
|
+
# [:porthole, :etag, "taxed_value"],
|
77
|
+
# [:static, ", after taxes.\n"]]]]
|
78
|
+
def compile!(exp)
|
79
|
+
case exp.first
|
80
|
+
when :multi
|
81
|
+
exp[1..-1].map { |e| compile!(e) }.join
|
82
|
+
when :static
|
83
|
+
str(exp[1])
|
84
|
+
when :porthole
|
85
|
+
send("on_#{exp[1]}", *exp[2..-1])
|
86
|
+
else
|
87
|
+
raise "Unhandled exp: #{exp.first}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Callback fired when the compiler finds a section token. We're
|
92
|
+
# passed the section name and the array of tokens.
|
93
|
+
def on_section(name, content, raw, delims)
|
94
|
+
# Convert the tokenized content of this section into a Ruby
|
95
|
+
# string we can use.
|
96
|
+
code = compile(content)
|
97
|
+
|
98
|
+
# Compile the Ruby for this section now that we know what's
|
99
|
+
# inside the section.
|
100
|
+
ev(<<-compiled)
|
101
|
+
if v = #{compile!(name)}
|
102
|
+
if v == true
|
103
|
+
#{code}
|
104
|
+
elsif v.is_a?(Proc)
|
105
|
+
t = Porthole::Template.new(v.call(#{raw.inspect}).to_s)
|
106
|
+
def t.tokens(src=@source)
|
107
|
+
p = Parser.new
|
108
|
+
p.otag, p.ctag = #{delims.inspect}
|
109
|
+
p.compile(src)
|
110
|
+
end
|
111
|
+
t.render(ctx.dup)
|
112
|
+
else
|
113
|
+
# Shortcut when passed non-array
|
114
|
+
v = [v] unless v.is_a?(Array) || defined?(Enumerator) && v.is_a?(Enumerator)
|
115
|
+
|
116
|
+
v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
|
117
|
+
end
|
118
|
+
end
|
119
|
+
compiled
|
120
|
+
end
|
121
|
+
|
122
|
+
# Fired when we find an inverted section. Just like `on_section`,
|
123
|
+
# we're passed the inverted section name and the array of tokens.
|
124
|
+
def on_inverted_section(name, content, raw, _)
|
125
|
+
# Convert the tokenized content of this section into a Ruby
|
126
|
+
# string we can use.
|
127
|
+
code = compile(content)
|
128
|
+
|
129
|
+
# Compile the Ruby for this inverted section now that we know
|
130
|
+
# what's inside.
|
131
|
+
ev(<<-compiled)
|
132
|
+
v = #{compile!(name)}
|
133
|
+
if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
|
134
|
+
#{code}
|
135
|
+
end
|
136
|
+
compiled
|
137
|
+
end
|
138
|
+
|
139
|
+
# Fired when the compiler finds a partial. We want to return code
|
140
|
+
# which calls a partial at runtime instead of expanding and
|
141
|
+
# including the partial's body to allow for recursive partials.
|
142
|
+
def on_partial(name, indentation)
|
143
|
+
ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
|
144
|
+
end
|
145
|
+
|
146
|
+
# An unescaped tag.
|
147
|
+
def on_utag(name)
|
148
|
+
ev(<<-compiled)
|
149
|
+
v = #{compile!(name)}
|
150
|
+
if v.is_a?(Proc)
|
151
|
+
v = Porthole::Template.new(v.call.to_s).render(ctx.dup)
|
152
|
+
end
|
153
|
+
v.to_s
|
154
|
+
compiled
|
155
|
+
end
|
156
|
+
|
157
|
+
# An escaped tag.
|
158
|
+
def on_etag(name)
|
159
|
+
ev(<<-compiled)
|
160
|
+
v = #{compile!(name)}
|
161
|
+
if v.is_a?(Proc)
|
162
|
+
v = Porthole::Template.new(v.call.to_s).render(ctx.dup)
|
163
|
+
end
|
164
|
+
ctx.escapeHTML(v.to_s)
|
165
|
+
compiled
|
166
|
+
end
|
167
|
+
|
168
|
+
def on_fetch(names)
|
169
|
+
names = names.map { |n| n.to_sym }
|
170
|
+
|
171
|
+
if names.length == 0
|
172
|
+
"ctx[:to_s]"
|
173
|
+
elsif names.length == 1
|
174
|
+
"ctx[#{names.first.to_sym.inspect}]"
|
175
|
+
else
|
176
|
+
initial, *rest = names
|
177
|
+
<<-compiled
|
178
|
+
#{rest.inspect}.inject(ctx[#{initial.inspect}]) { |value, key|
|
179
|
+
value && ctx.find(value, key)
|
180
|
+
}
|
181
|
+
compiled
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# An interpolation-friendly version of a string, for use within a
|
186
|
+
# Ruby string.
|
187
|
+
def ev(s)
|
188
|
+
"#\{#{s}}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def str(s)
|
192
|
+
s.inspect[1..-2]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class Porthole
|
4
|
+
# The Parser is responsible for taking a string template and
|
5
|
+
# converting it into an array of tokens and, really, expressions. It
|
6
|
+
# raises SyntaxError if there is anything it doesn't understand and
|
7
|
+
# knows which sigil corresponds to which tag type.
|
8
|
+
#
|
9
|
+
# For example, given this template:
|
10
|
+
#
|
11
|
+
# Hi {{thing}}!
|
12
|
+
#
|
13
|
+
# Run through the Parser we'll get these tokens:
|
14
|
+
#
|
15
|
+
# [:multi,
|
16
|
+
# [:static, "Hi "],
|
17
|
+
# [:porthole, :etag, "thing"],
|
18
|
+
# [:static, "!\n"]]
|
19
|
+
#
|
20
|
+
# You can see the array of tokens for any template with the
|
21
|
+
# porthole(1) command line tool:
|
22
|
+
#
|
23
|
+
# $ porthole --tokens test.porthole
|
24
|
+
# [:multi, [:static, "Hi "], [:porthole, :etag, "thing"], [:static, "!\n"]]
|
25
|
+
class Parser
|
26
|
+
# A SyntaxError is raised when the Parser comes across unclosed
|
27
|
+
# tags, sections, illegal content in tags, or anything of that
|
28
|
+
# sort.
|
29
|
+
class SyntaxError < StandardError
|
30
|
+
def initialize(message, position)
|
31
|
+
@message = message
|
32
|
+
@lineno, @column, @line, _ = position
|
33
|
+
@stripped_line = @line.strip
|
34
|
+
@stripped_column = @column - (@line.size - @line.lstrip.size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
<<-EOF
|
39
|
+
#{@message}
|
40
|
+
Line #{@lineno}
|
41
|
+
#{@stripped_line}
|
42
|
+
#{' ' * @stripped_column}^
|
43
|
+
EOF
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# After these types of tags, all whitespace until the end of the line will
|
48
|
+
# be skipped if they are the first (and only) non-whitespace content on
|
49
|
+
# the line.
|
50
|
+
SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ]
|
51
|
+
|
52
|
+
# The content allowed in a tag name.
|
53
|
+
ALLOWED_CONTENT = /(\w|[?!\/.-])*/
|
54
|
+
|
55
|
+
# These types of tags allow any content,
|
56
|
+
# the rest only allow ALLOWED_CONTENT.
|
57
|
+
ANY_CONTENT = [ '!', '=' ]
|
58
|
+
|
59
|
+
attr_reader :scanner, :result
|
60
|
+
attr_writer :otag, :ctag
|
61
|
+
|
62
|
+
# Accepts an options hash which does nothing but may be used in
|
63
|
+
# the future.
|
64
|
+
def initialize(options = {})
|
65
|
+
@options = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
# The opening tag delimiter. This may be changed at runtime.
|
69
|
+
def otag
|
70
|
+
@otag ||= '%%'
|
71
|
+
end
|
72
|
+
|
73
|
+
# The closing tag delimiter. This too may be changed at runtime.
|
74
|
+
def ctag
|
75
|
+
@ctag ||= '%%'
|
76
|
+
end
|
77
|
+
|
78
|
+
# Given a string template, returns an array of tokens.
|
79
|
+
def compile(template)
|
80
|
+
if template.respond_to?(:encoding)
|
81
|
+
@encoding = template.encoding
|
82
|
+
template = template.dup.force_encoding("BINARY")
|
83
|
+
else
|
84
|
+
@encoding = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Keeps information about opened sections.
|
88
|
+
@sections = []
|
89
|
+
@result = [:multi]
|
90
|
+
@scanner = StringScanner.new(template)
|
91
|
+
|
92
|
+
# Scan until the end of the template.
|
93
|
+
until @scanner.eos?
|
94
|
+
scan_tags || scan_text
|
95
|
+
end
|
96
|
+
|
97
|
+
if !@sections.empty?
|
98
|
+
# We have parsed the whole file, but there's still opened sections.
|
99
|
+
type, pos, result = @sections.pop
|
100
|
+
error "Unclosed section #{type.inspect}", pos
|
101
|
+
end
|
102
|
+
|
103
|
+
@result
|
104
|
+
end
|
105
|
+
|
106
|
+
# Find {{portholes}} and add them to the @result array.
|
107
|
+
def scan_tags
|
108
|
+
# Scan until we hit an opening delimiter.
|
109
|
+
start_of_line = @scanner.beginning_of_line?
|
110
|
+
pre_match_position = @scanner.pos
|
111
|
+
last_index = @result.length
|
112
|
+
|
113
|
+
return unless x = @scanner.scan(/([ \t]*)?#{Regexp.escape(otag)}/)
|
114
|
+
padding = @scanner[1] || ''
|
115
|
+
|
116
|
+
# Don't touch the preceding whitespace unless we're matching the start
|
117
|
+
# of a new line.
|
118
|
+
unless start_of_line
|
119
|
+
@result << [:static, padding] unless padding.empty?
|
120
|
+
pre_match_position += padding.length
|
121
|
+
padding = ''
|
122
|
+
end
|
123
|
+
|
124
|
+
# Since {{= rewrites ctag, we store the ctag which should be used
|
125
|
+
# when parsing this specific tag.
|
126
|
+
current_ctag = self.ctag
|
127
|
+
type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
|
128
|
+
@scanner.skip(/\s*/)
|
129
|
+
|
130
|
+
# ANY_CONTENT tags allow any character inside of them, while
|
131
|
+
# other tags (such as variables) are more strict.
|
132
|
+
if ANY_CONTENT.include?(type)
|
133
|
+
r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
|
134
|
+
content = scan_until_exclusive(r)
|
135
|
+
else
|
136
|
+
content = @scanner.scan(ALLOWED_CONTENT)
|
137
|
+
end
|
138
|
+
|
139
|
+
# We found {{ but we can't figure out what's going on inside.
|
140
|
+
error "Illegal content in tag" if content.empty?
|
141
|
+
|
142
|
+
fetch = [:porthole, :fetch, content.split('.')]
|
143
|
+
prev = @result
|
144
|
+
|
145
|
+
# Based on the sigil, do what needs to be done.
|
146
|
+
case type
|
147
|
+
when '#'
|
148
|
+
block = [:multi]
|
149
|
+
@result << [:porthole, :section, fetch, block]
|
150
|
+
@sections << [content, position, @result]
|
151
|
+
@result = block
|
152
|
+
when '^'
|
153
|
+
block = [:multi]
|
154
|
+
@result << [:porthole, :inverted_section, fetch, block]
|
155
|
+
@sections << [content, position, @result]
|
156
|
+
@result = block
|
157
|
+
when '/'
|
158
|
+
section, pos, result = @sections.pop
|
159
|
+
raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
|
160
|
+
(@result = result).last << raw << [self.otag, self.ctag]
|
161
|
+
|
162
|
+
if section.nil?
|
163
|
+
error "Closing unopened #{content.inspect}"
|
164
|
+
elsif section != content
|
165
|
+
error "Unclosed section #{section.inspect}", pos
|
166
|
+
end
|
167
|
+
when '!'
|
168
|
+
# ignore comments
|
169
|
+
when '='
|
170
|
+
self.otag, self.ctag = content.split(' ', 2)
|
171
|
+
when '>', '<'
|
172
|
+
@result << [:porthole, :partial, content, padding]
|
173
|
+
when '{', '&'
|
174
|
+
# The closing } in unescaped tags is just a hack for
|
175
|
+
# aesthetics.
|
176
|
+
type = "}" if type == "{"
|
177
|
+
@result << [:porthole, :utag, fetch]
|
178
|
+
else
|
179
|
+
@result << [:porthole, :etag, fetch]
|
180
|
+
end
|
181
|
+
|
182
|
+
# Skip whitespace and any balancing sigils after the content
|
183
|
+
# inside this tag.
|
184
|
+
@scanner.skip(/\s+/)
|
185
|
+
@scanner.skip(regexp(type)) if type
|
186
|
+
|
187
|
+
# Try to find the closing tag.
|
188
|
+
unless close = @scanner.scan(regexp(current_ctag))
|
189
|
+
error "Unclosed tag"
|
190
|
+
end
|
191
|
+
|
192
|
+
# If this tag was the only non-whitespace content on this line, strip
|
193
|
+
# the remaining whitespace. If not, but we've been hanging on to padding
|
194
|
+
# from the beginning of the line, re-insert the padding as static text.
|
195
|
+
if start_of_line && !@scanner.eos?
|
196
|
+
if @scanner.peek(2) =~ /\r?\n/ && SKIP_WHITESPACE.include?(type)
|
197
|
+
@scanner.skip(/\r?\n/)
|
198
|
+
else
|
199
|
+
prev.insert(last_index, [:static, padding]) unless padding.empty?
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Store off the current scanner position now that we've closed the tag
|
204
|
+
# and consumed any irrelevant whitespace.
|
205
|
+
@sections.last[1] << @scanner.pos unless @sections.empty?
|
206
|
+
|
207
|
+
return unless @result == [:multi]
|
208
|
+
end
|
209
|
+
|
210
|
+
# Try to find static text, e.g. raw HTML with no {{portholes}}.
|
211
|
+
def scan_text
|
212
|
+
text = scan_until_exclusive(/(^[ \t]*)?#{Regexp.escape(otag)}/)
|
213
|
+
|
214
|
+
if text.nil?
|
215
|
+
# Couldn't find any otag, which means the rest is just static text.
|
216
|
+
text = @scanner.rest
|
217
|
+
# Mark as done.
|
218
|
+
@scanner.terminate
|
219
|
+
end
|
220
|
+
|
221
|
+
text.force_encoding(@encoding) if @encoding
|
222
|
+
|
223
|
+
@result << [:static, text] unless text.empty?
|
224
|
+
end
|
225
|
+
|
226
|
+
# Scans the string until the pattern is matched. Returns the substring
|
227
|
+
# *excluding* the end of the match, advancing the scan pointer to that
|
228
|
+
# location. If there is no match, nil is returned.
|
229
|
+
def scan_until_exclusive(regexp)
|
230
|
+
pos = @scanner.pos
|
231
|
+
if @scanner.scan_until(regexp)
|
232
|
+
@scanner.pos -= @scanner.matched.size
|
233
|
+
@scanner.pre_match[pos..-1]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns [lineno, column, line]
|
238
|
+
def position
|
239
|
+
# The rest of the current line
|
240
|
+
rest = @scanner.check_until(/\n|\Z/).to_s.chomp
|
241
|
+
|
242
|
+
# What we have parsed so far
|
243
|
+
parsed = @scanner.string[0...@scanner.pos]
|
244
|
+
|
245
|
+
lines = parsed.split("\n")
|
246
|
+
|
247
|
+
[ lines.size, lines.last.size - 1, lines.last + rest ]
|
248
|
+
end
|
249
|
+
|
250
|
+
# Used to quickly convert a string into a regular expression
|
251
|
+
# usable by the string scanner.
|
252
|
+
def regexp(thing)
|
253
|
+
/#{Regexp.escape(thing)}/
|
254
|
+
end
|
255
|
+
|
256
|
+
# Raises a SyntaxError. The message should be the name of the
|
257
|
+
# error - other details such as line number and position are
|
258
|
+
# handled for you.
|
259
|
+
def error(message, pos = position)
|
260
|
+
raise SyntaxError.new(message, pos)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|