scribble 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +455 -0
- data/Rakefile +2 -0
- data/lib/scribble.rb +44 -0
- data/lib/scribble/block.rb +25 -0
- data/lib/scribble/converter.rb +10 -0
- data/lib/scribble/errors.rb +24 -0
- data/lib/scribble/method.rb +91 -0
- data/lib/scribble/methods/if.rb +26 -0
- data/lib/scribble/methods/layout.rb +25 -0
- data/lib/scribble/methods/partial.rb +14 -0
- data/lib/scribble/methods/times.rb +11 -0
- data/lib/scribble/nodes/call.rb +55 -0
- data/lib/scribble/nodes/ending.rb +6 -0
- data/lib/scribble/nodes/node.rb +24 -0
- data/lib/scribble/nodes/value.rb +16 -0
- data/lib/scribble/objects/boolean.rb +33 -0
- data/lib/scribble/objects/fixnum.rb +53 -0
- data/lib/scribble/objects/nil.rb +21 -0
- data/lib/scribble/objects/string.rb +62 -0
- data/lib/scribble/parsing/nester.rb +49 -0
- data/lib/scribble/parsing/parser.rb +132 -0
- data/lib/scribble/parsing/reporter.rb +71 -0
- data/lib/scribble/parsing/transform.rb +87 -0
- data/lib/scribble/partial.rb +41 -0
- data/lib/scribble/registry.rb +95 -0
- data/lib/scribble/support/context.rb +98 -0
- data/lib/scribble/support/matcher.rb +74 -0
- data/lib/scribble/support/unmatched.rb +70 -0
- data/lib/scribble/support/utilities.rb +49 -0
- data/lib/scribble/template.rb +61 -0
- data/lib/scribble/version.rb +3 -0
- data/scribble.gemspec +22 -0
- data/test/all.rb +23 -0
- data/test/errors_test.rb +94 -0
- data/test/methods/if_test.rb +49 -0
- data/test/methods/layout_test.rb +71 -0
- data/test/methods/partial_test.rb +85 -0
- data/test/methods/times_test.rb +10 -0
- data/test/objects/boolean_test.rb +162 -0
- data/test/objects/fixnum_test.rb +236 -0
- data/test/objects/nil_test.rb +83 -0
- data/test/objects/string_test.rb +268 -0
- data/test/parsing/parsing_test.rb +234 -0
- data/test/registry_test.rb +264 -0
- data/test/template_test.rb +51 -0
- data/test/test_helper.rb +65 -0
- metadata +127 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Scribble
|
2
|
+
Registry.for NilClass do
|
3
|
+
name 'nothing'
|
4
|
+
|
5
|
+
to_boolean { false }
|
6
|
+
to_string { '' }
|
7
|
+
|
8
|
+
# Logical operators
|
9
|
+
method :or, Object, cast: 'to_boolean'
|
10
|
+
method :and, Object, cast: 'to_boolean'
|
11
|
+
|
12
|
+
# Equality
|
13
|
+
method :equals, NilClass, returns: true
|
14
|
+
method :differs, NilClass, returns: false
|
15
|
+
method :equals, Object, returns: false
|
16
|
+
method :differs, Object, returns: true
|
17
|
+
|
18
|
+
# Unary operators
|
19
|
+
method :not, cast: 'to_boolean'
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Scribble
|
2
|
+
Registry.for String do
|
3
|
+
to_boolean { !empty? }
|
4
|
+
to_string { self }
|
5
|
+
|
6
|
+
# Logical operators
|
7
|
+
method :or, Object, cast: 'to_boolean'
|
8
|
+
method :and, Object, cast: 'to_boolean'
|
9
|
+
|
10
|
+
# Equality
|
11
|
+
method :equals, String, as: '=='
|
12
|
+
method :differs, String, as: '!='
|
13
|
+
method :equals, Object, returns: false
|
14
|
+
method :differs, Object, returns: true
|
15
|
+
|
16
|
+
# Unary operators
|
17
|
+
method :not, cast: 'to_boolean'
|
18
|
+
|
19
|
+
# Size and length
|
20
|
+
method :size
|
21
|
+
method :length
|
22
|
+
method :empty, as: 'empty?'
|
23
|
+
|
24
|
+
# Concatenation
|
25
|
+
method :add, Fixnum, to: ->(fixnum) { self + fixnum.to_s }
|
26
|
+
method :add, String, as: '+'
|
27
|
+
method :append, String, as: '+'
|
28
|
+
method :prepend, String
|
29
|
+
|
30
|
+
# Repetition
|
31
|
+
method :multiply, Fixnum, to: ->(count) { Support::Utilities.repeat self, count }
|
32
|
+
method :repeat, Fixnum, to: ->(count) { Support::Utilities.repeat self, count }
|
33
|
+
|
34
|
+
# Truncation
|
35
|
+
method :truncate, Fixnum, to: ->(length) { Support::Utilities.truncate self, false, length, ' ...' }
|
36
|
+
method :truncate, Fixnum, String, to: ->(length, omission) { Support::Utilities.truncate self, false, length, omission }
|
37
|
+
method :truncate_words, Fixnum, to: ->(length) { Support::Utilities.truncate self, true, length, ' ...' }
|
38
|
+
method :truncate_words, Fixnum, String, to: ->(length, omission) { Support::Utilities.truncate self, true, length, omission }
|
39
|
+
|
40
|
+
# Replacement
|
41
|
+
method :replace, String, String, to: ->(replace, with) { self.gsub replace, with }
|
42
|
+
method :replace_first, String, String, to: ->(replace, with) { self.sub replace, with }
|
43
|
+
|
44
|
+
# Removal
|
45
|
+
method :subtract, String, to: ->(remove) { self.gsub remove, '' }
|
46
|
+
method :remove, String, to: ->(remove) { self.gsub remove, '' }
|
47
|
+
method :remove_first, String, to: ->(remove) { self.sub remove, '' }
|
48
|
+
|
49
|
+
# Case manipulation
|
50
|
+
method :capitalize
|
51
|
+
method :upcase
|
52
|
+
method :downcase
|
53
|
+
|
54
|
+
# Reversal
|
55
|
+
method :reverse
|
56
|
+
|
57
|
+
# Whitespace removal
|
58
|
+
method :strip
|
59
|
+
method :strip_left, as: 'lstrip'
|
60
|
+
method :strip_right, as: 'rstrip'
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Parsing
|
3
|
+
class Nester
|
4
|
+
def initialize nodes
|
5
|
+
@nodes = nodes
|
6
|
+
end
|
7
|
+
|
8
|
+
def nodes root = true
|
9
|
+
[].tap do |nodes|
|
10
|
+
while step(root)
|
11
|
+
nodes << node
|
12
|
+
if node.block?
|
13
|
+
@current = node
|
14
|
+
node.nodes = nodes false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def step root
|
23
|
+
@cursor = @cursor ? @cursor + 1 : 0
|
24
|
+
|
25
|
+
if node.is_a? Nodes::Ending
|
26
|
+
raise unexpected_end if root
|
27
|
+
false
|
28
|
+
elsif node.nil?
|
29
|
+
raise unexpected_eot unless root
|
30
|
+
false
|
31
|
+
else
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def node
|
37
|
+
@nodes[@cursor]
|
38
|
+
end
|
39
|
+
|
40
|
+
def unexpected_end
|
41
|
+
Errors::Syntax.new "Unexpected 'end' #{node.line_and_column}; no block currently open"
|
42
|
+
end
|
43
|
+
|
44
|
+
def unexpected_eot
|
45
|
+
Errors::Syntax.new "Unexpected end of template; unclosed '#{@current.name}' block #{@current.line_and_column}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Scribble
|
4
|
+
module Parsing
|
5
|
+
class DelimitedChunk < Parslet::Atoms::Base
|
6
|
+
def initialize delimiters, min_chars = 0
|
7
|
+
@delimiters = delimiters
|
8
|
+
@min_chars = min_chars
|
9
|
+
end
|
10
|
+
|
11
|
+
def try(source, context, consume_all)
|
12
|
+
excluding_length = @delimiters.map {|d| source.chars_until(d) }.min
|
13
|
+
|
14
|
+
if excluding_length >= @min_chars
|
15
|
+
return succ(source.consume([excluding_length, 100000].min)) # max 100000 character atom, otherwise error in resulting regexp
|
16
|
+
else
|
17
|
+
return context.err(self, source, "No such string in input: #{@delimiters.inspect}.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s_inner(prec)
|
22
|
+
"until('#{@delimiters.inspect}')"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Parser < Parslet::Parser
|
27
|
+
root(:template)
|
28
|
+
|
29
|
+
# Text, tags and endings
|
30
|
+
rule(:template) { (text | ending | tag).repeat.as(:template) }
|
31
|
+
rule(:text) { DelimitedChunk.new(['{{'], 1).as(:text) }
|
32
|
+
rule(:ending) { ltag >> str('end').as(:ending) >> rtag }
|
33
|
+
rule(:tag) { ltag >> (operation >> rtag | tag_command | rtag) }
|
34
|
+
|
35
|
+
# Command ending in rtag
|
36
|
+
rule(:tag_command) { tag_simple_command | (unary >> tag_command_tail).as(:chain) }
|
37
|
+
rule(:tag_command_tail) { dot >> (tag_simple_command.repeat(1,1) | simple_function >> tag_command_tail) }
|
38
|
+
rule(:tag_simple_command) { (name >> space >> tag_args).as(:call) }
|
39
|
+
|
40
|
+
# Command ending in rparen
|
41
|
+
rule(:paren_command) { paren_simple_command | (unary >> paren_command_tail).as(:chain) }
|
42
|
+
rule(:paren_command_tail) { dot >> (paren_simple_command.repeat(1,1) | simple_function >> paren_command_tail) }
|
43
|
+
rule(:paren_simple_command) { (name >> space >> paren_args).as(:call) }
|
44
|
+
|
45
|
+
# Entry point to operations
|
46
|
+
rule(:operation) { logical_or }
|
47
|
+
|
48
|
+
# Or / and
|
49
|
+
rule(:logical_or) { (logical_and >> ((pipe.as(:op) >> logical_and.as(:arg)).as(:or)).repeat(1)).as(:chain) | logical_and }
|
50
|
+
rule(:logical_and) { (equality >> ((ampersand.as(:op) >> equality.as(:arg)).as(:and)).repeat(1)).as(:chain) | equality }
|
51
|
+
|
52
|
+
# Equality / inequality
|
53
|
+
rule(:equality) { (comparison >> (equals | differs).repeat(1)).as(:chain) | comparison }
|
54
|
+
rule(:equals) { (equals_sign.as(:op) >> comparison.as(:arg)).as(:equals) }
|
55
|
+
rule(:differs) { (bang_equals.as(:op) >> comparison.as(:arg)).as(:differs) }
|
56
|
+
|
57
|
+
# Comparisons
|
58
|
+
rule(:comparison) { (additive >> (greater | less | greater_or_equal | less_or_equal).repeat(1)).as(:chain) | additive }
|
59
|
+
rule(:greater) { (rangle.as(:op) >> additive.as(:arg)).as(:greater) }
|
60
|
+
rule(:less) { (langle.as(:op) >> additive.as(:arg)).as(:less) }
|
61
|
+
rule(:greater_or_equal) { (rangle_equals.as(:op) >> additive.as(:arg)).as(:greater_or_equal) }
|
62
|
+
rule(:less_or_equal) { (langle_equals.as(:op) >> additive.as(:arg)).as(:less_or_equal) }
|
63
|
+
|
64
|
+
# Add / subtract
|
65
|
+
rule(:additive) { (multitive >> (add | subtract).repeat(1)).as(:chain) | multitive }
|
66
|
+
rule(:add) { (plus.as(:op) >> multitive.as(:arg)).as(:add) }
|
67
|
+
rule(:subtract) { (dash.as(:op) >> multitive.as(:arg)).as(:subtract) }
|
68
|
+
|
69
|
+
# Multiply / divide
|
70
|
+
rule(:multitive) { (function >> (multiply | divide | remainder).repeat(1)).as(:chain) | function }
|
71
|
+
rule(:multiply) { (asterisk.as(:op) >> function.as(:arg)).as(:multiply) }
|
72
|
+
rule(:divide) { (slash.as(:op) >> function.as(:arg)).as(:divide) }
|
73
|
+
rule(:remainder) { (percent.as(:op) >> function.as(:arg)).as(:remainder) }
|
74
|
+
|
75
|
+
# Functions
|
76
|
+
rule(:function) { (unary >> (dot >> simple_function).repeat(1)).as(:chain) | unary }
|
77
|
+
rule(:simple_function) { (name >> lparen >> (rparen | paren_args)).as(:call) | (name >> space?).as(:call_or_variable) }
|
78
|
+
|
79
|
+
# Unary operators
|
80
|
+
rule(:unary) { unary_operand | unary_negative | unary_not }
|
81
|
+
rule(:unary_negative) { (dash.as(:op) >> unary.as(:receiver)).as(:negative) }
|
82
|
+
rule(:unary_not) { (bang.as(:op) >> unary.as(:receiver)).as(:not) }
|
83
|
+
|
84
|
+
# Unary operand
|
85
|
+
rule(:unary_operand) { value >> space? | parentheses | simple_function }
|
86
|
+
rule(:parentheses) { lparen >> (operation >> rparen | paren_command) }
|
87
|
+
|
88
|
+
# Values
|
89
|
+
rule(:value) { str('true').as(:true) | str('false').as(:false) | empty_string | string | number }
|
90
|
+
rule(:string) { str("'") >> (str("\\") >> any | str("'").absent? >> any).repeat(1).as(:string) >> str("'") }
|
91
|
+
rule(:empty_string) { str("'") >> str('').as(:string) >> str("'") }
|
92
|
+
rule(:number) { match['0-9'].repeat(1).as(:number) }
|
93
|
+
|
94
|
+
# Name and arguments
|
95
|
+
rule(:name) { (match['a-z'] >> match['a-z0-9_'].repeat >> match['?!'].maybe).as(:name) }
|
96
|
+
rule(:paren_args) { (operation >> (comma >> operation).repeat >> rparen | paren_command).as(:args) }
|
97
|
+
rule(:tag_args) { (operation >> (comma >> operation).repeat >> rtag | tag_command).as(:args) }
|
98
|
+
|
99
|
+
# Spaces
|
100
|
+
rule(:space) { match('\s').repeat(1) }
|
101
|
+
rule(:space?) { space.maybe }
|
102
|
+
|
103
|
+
# Tag delimiters
|
104
|
+
rule(:ltag) { str('{{') >> space? }
|
105
|
+
rule(:rtag) { space? >> str('}}') }
|
106
|
+
|
107
|
+
# Parens
|
108
|
+
rule(:lparen) { str('(') >> space? }
|
109
|
+
rule(:rparen) { str(')') >> space? }
|
110
|
+
|
111
|
+
# Dot, comma
|
112
|
+
rule(:comma) { str(',') >> space? }
|
113
|
+
rule(:dot) { str('.') }
|
114
|
+
|
115
|
+
# Operators
|
116
|
+
rule(:pipe) { str('|') >> space? }
|
117
|
+
rule(:ampersand) { str('&') >> space? }
|
118
|
+
rule(:equals_sign) { str('=') >> space? }
|
119
|
+
rule(:bang_equals) { str('!=') >> space? }
|
120
|
+
rule(:langle) { str('<') >> space? }
|
121
|
+
rule(:rangle) { str('>') >> space? }
|
122
|
+
rule(:langle_equals) { str('<=') >> space? }
|
123
|
+
rule(:rangle_equals) { str('>=') >> space? }
|
124
|
+
rule(:plus) { str('+') >> space? }
|
125
|
+
rule(:dash) { str('-') >> space? }
|
126
|
+
rule(:asterisk) { str('*') >> space? }
|
127
|
+
rule(:slash) { str('/') >> space? }
|
128
|
+
rule(:percent) { str('%') >> space? }
|
129
|
+
rule(:bang) { str('!') >> space? }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Parsing
|
3
|
+
class Reporter
|
4
|
+
Cause = Struct.new :source, :position do
|
5
|
+
def strings
|
6
|
+
@strings ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def raise
|
10
|
+
Kernel.raise Errors::Syntax.new "#{unexpected} at line #{line} column #{column}#{explanation}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def unexpected
|
14
|
+
if character
|
15
|
+
"Unexpected '#{character}'"
|
16
|
+
else
|
17
|
+
"Unexpected end of template"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def character
|
22
|
+
source.instance_variable_get(:@str).string[position]
|
23
|
+
end
|
24
|
+
|
25
|
+
def line
|
26
|
+
source.line_and_column(position).first
|
27
|
+
end
|
28
|
+
|
29
|
+
def column
|
30
|
+
source.line_and_column(position).last
|
31
|
+
end
|
32
|
+
|
33
|
+
def explanation
|
34
|
+
if unterminated_string?
|
35
|
+
"; unterminated string"
|
36
|
+
else
|
37
|
+
", expected #{Support::Utilities.to_sentence expected}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def unterminated_string?
|
42
|
+
strings.all? { |string| ["\\", "'"].include? string }
|
43
|
+
end
|
44
|
+
|
45
|
+
def expected
|
46
|
+
strings.map do |string|
|
47
|
+
if string == 'true' then "a value"
|
48
|
+
elsif string == '+' then "an operator"
|
49
|
+
elsif string == '(' then "'('"
|
50
|
+
elsif string == '}}' then "'}}'"
|
51
|
+
elsif string == 'end' then "'end'"
|
52
|
+
end
|
53
|
+
end.compact.uniq.sort
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def err atom, source, message, children = nil
|
58
|
+
err_at atom, source, message, source.pos, children
|
59
|
+
end
|
60
|
+
|
61
|
+
def err_at atom, source, message, position, children = nil
|
62
|
+
if @position.nil? || position > @position
|
63
|
+
@position = position
|
64
|
+
@cause = Cause.new source, position
|
65
|
+
end
|
66
|
+
@cause.strings << atom.str if position == @position && atom.respond_to?(:str)
|
67
|
+
@cause
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Scribble
|
4
|
+
module Parsing
|
5
|
+
class Transform < Parslet::Transform
|
6
|
+
|
7
|
+
# Template, text and endings
|
8
|
+
|
9
|
+
rule template: sequence(:nodes) do
|
10
|
+
Nester.new nodes
|
11
|
+
end
|
12
|
+
|
13
|
+
rule text: simple(:slice) do
|
14
|
+
Nodes::Value.new slice, slice.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
rule ending: simple(:slice) do
|
18
|
+
Nodes::Ending.new slice
|
19
|
+
end
|
20
|
+
|
21
|
+
# Binary operators
|
22
|
+
|
23
|
+
%i(or and equals differs
|
24
|
+
greater less greater_or_equal less_or_equal
|
25
|
+
add subtract multiply divide remainder
|
26
|
+
).each do |operator|
|
27
|
+
rule operator => {op: simple(:slice), arg: simple(:arg)} do
|
28
|
+
Nodes::Call.new slice, operator, args: [arg]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Unary operators
|
33
|
+
|
34
|
+
%i(negative not).each do |operator|
|
35
|
+
rule operator => {op: simple(:slice), receiver: simple(:receiver)} do
|
36
|
+
Nodes::Call.new slice, operator, receiver: receiver
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Left associative operation reduction
|
41
|
+
|
42
|
+
rule chain: sequence(:calls) do
|
43
|
+
calls.reduce calls.shift do |base, call|
|
44
|
+
call.receiver = base; call
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Calls with no, one or multiple arguments
|
49
|
+
|
50
|
+
rule call: {name: simple(:slice)} do
|
51
|
+
Nodes::Call.new slice, slice.to_sym
|
52
|
+
end
|
53
|
+
|
54
|
+
rule call: {name: simple(:slice), args: simple(:arg)} do
|
55
|
+
Nodes::Call.new slice, slice.to_sym, args: [arg]
|
56
|
+
end
|
57
|
+
|
58
|
+
rule call: {name: simple(:slice), args: sequence(:args)} do
|
59
|
+
Nodes::Call.new slice, slice.to_sym, args: args
|
60
|
+
end
|
61
|
+
|
62
|
+
# Call without arguments or parentheses
|
63
|
+
|
64
|
+
rule call_or_variable: {name: simple(:slice)} do
|
65
|
+
Nodes::Call.new slice, slice.to_sym, allow_variable: true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Values
|
69
|
+
|
70
|
+
rule number: simple(:slice) do
|
71
|
+
Nodes::Value.new slice, slice.to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
rule string: simple(:slice) do
|
75
|
+
Nodes::Value.new slice, slice.to_s.gsub(/\\(.)/, '\1')
|
76
|
+
end
|
77
|
+
|
78
|
+
rule true: simple(:slice) do
|
79
|
+
Nodes::Value.new slice, true
|
80
|
+
end
|
81
|
+
|
82
|
+
rule false: simple(:slice) do
|
83
|
+
Nodes::Value.new slice, false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Scribble
|
2
|
+
class Partial
|
3
|
+
attr_reader :format
|
4
|
+
|
5
|
+
def initialize source, format: nil
|
6
|
+
@source, @format = source, format
|
7
|
+
end
|
8
|
+
|
9
|
+
# Parse and transform
|
10
|
+
|
11
|
+
def parse
|
12
|
+
@parse ||= Parsing::Parser.new.parse @source, reporter: Parsing::Reporter.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def transform
|
16
|
+
@transform ||= Parsing::Transform.new.apply parse
|
17
|
+
end
|
18
|
+
|
19
|
+
def nodes
|
20
|
+
@nodes ||= transform.nodes
|
21
|
+
end
|
22
|
+
|
23
|
+
# Partial context
|
24
|
+
|
25
|
+
class Context
|
26
|
+
include Support::Context
|
27
|
+
|
28
|
+
def initialize partial, context
|
29
|
+
@partial, @context = partial, context
|
30
|
+
end
|
31
|
+
|
32
|
+
def nodes
|
33
|
+
@partial.nodes
|
34
|
+
end
|
35
|
+
|
36
|
+
def format
|
37
|
+
@partial.format || @context.format
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|