brandish 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +9 -0
- data/bin/brandish +16 -0
- data/brandish.gemspec +39 -0
- data/defaults/templates/html.liquid +39 -0
- data/lib/brandish.rb +51 -0
- data/lib/brandish/application.rb +163 -0
- data/lib/brandish/application/bench_command.rb +96 -0
- data/lib/brandish/application/build_command.rb +73 -0
- data/lib/brandish/application/initialize_command.rb +83 -0
- data/lib/brandish/application/serve_command.rb +150 -0
- data/lib/brandish/configure.rb +196 -0
- data/lib/brandish/configure/dsl.rb +135 -0
- data/lib/brandish/configure/dsl/form.rb +136 -0
- data/lib/brandish/configure/form.rb +32 -0
- data/lib/brandish/errors.rb +65 -0
- data/lib/brandish/execute.rb +26 -0
- data/lib/brandish/markup.rb +10 -0
- data/lib/brandish/markup/redcarpet.rb +14 -0
- data/lib/brandish/markup/redcarpet/format.rb +127 -0
- data/lib/brandish/markup/redcarpet/html.rb +95 -0
- data/lib/brandish/parser.rb +26 -0
- data/lib/brandish/parser/main.rb +237 -0
- data/lib/brandish/parser/node.rb +89 -0
- data/lib/brandish/parser/node/block.rb +98 -0
- data/lib/brandish/parser/node/command.rb +102 -0
- data/lib/brandish/parser/node/pair.rb +42 -0
- data/lib/brandish/parser/node/root.rb +83 -0
- data/lib/brandish/parser/node/string.rb +18 -0
- data/lib/brandish/parser/node/text.rb +114 -0
- data/lib/brandish/path_set.rb +163 -0
- data/lib/brandish/processor.rb +47 -0
- data/lib/brandish/processor/base.rb +144 -0
- data/lib/brandish/processor/block.rb +47 -0
- data/lib/brandish/processor/command.rb +47 -0
- data/lib/brandish/processor/context.rb +169 -0
- data/lib/brandish/processor/descend.rb +32 -0
- data/lib/brandish/processor/inline.rb +49 -0
- data/lib/brandish/processor/name_filter.rb +67 -0
- data/lib/brandish/processor/pair_filter.rb +96 -0
- data/lib/brandish/processors.rb +26 -0
- data/lib/brandish/processors/all.rb +19 -0
- data/lib/brandish/processors/all/comment.rb +29 -0
- data/lib/brandish/processors/all/embed.rb +56 -0
- data/lib/brandish/processors/all/if.rb +109 -0
- data/lib/brandish/processors/all/import.rb +95 -0
- data/lib/brandish/processors/all/literal.rb +42 -0
- data/lib/brandish/processors/all/verify.rb +47 -0
- data/lib/brandish/processors/common.rb +20 -0
- data/lib/brandish/processors/common/asset.rb +118 -0
- data/lib/brandish/processors/common/asset/paths.rb +93 -0
- data/lib/brandish/processors/common/group.rb +67 -0
- data/lib/brandish/processors/common/header.rb +86 -0
- data/lib/brandish/processors/common/markup.rb +127 -0
- data/lib/brandish/processors/common/output.rb +73 -0
- data/lib/brandish/processors/html.rb +18 -0
- data/lib/brandish/processors/html/group.rb +33 -0
- data/lib/brandish/processors/html/header.rb +46 -0
- data/lib/brandish/processors/html/markup.rb +131 -0
- data/lib/brandish/processors/html/output.rb +62 -0
- data/lib/brandish/processors/html/output/document.rb +127 -0
- data/lib/brandish/processors/html/script.rb +64 -0
- data/lib/brandish/processors/html/script/babel.rb +48 -0
- data/lib/brandish/processors/html/script/coffee.rb +47 -0
- data/lib/brandish/processors/html/script/vanilla.rb +45 -0
- data/lib/brandish/processors/html/style.rb +82 -0
- data/lib/brandish/processors/html/style/highlight.rb +89 -0
- data/lib/brandish/processors/html/style/sass.rb +64 -0
- data/lib/brandish/processors/html/style/vanilla.rb +71 -0
- data/lib/brandish/processors/latex.rb +15 -0
- data/lib/brandish/processors/latex/markup.rb +47 -0
- data/lib/brandish/scanner.rb +64 -0
- data/lib/brandish/version.rb +9 -0
- data/templates/initialize/Gemfile.tt +14 -0
- data/templates/initialize/brandish.config.rb.tt +49 -0
- metadata +296 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Brandish
|
5
|
+
class Parser
|
6
|
+
class Node
|
7
|
+
# A "block" node. Basically, this is a node that encapsulates a block of
|
8
|
+
# text. This typically has a 1-to-1 relationship with the body of text,
|
9
|
+
# while a command may not.
|
10
|
+
class Block < Node
|
11
|
+
# The name of the block.
|
12
|
+
#
|
13
|
+
# @return [::String]
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# The body of the block.
|
17
|
+
#
|
18
|
+
# @return [Node::Root]
|
19
|
+
attr_reader :body
|
20
|
+
|
21
|
+
# The "pairs" of arguments to be passed to the command.
|
22
|
+
#
|
23
|
+
# @return [{::String => ::String}]
|
24
|
+
attr_reader :pairs
|
25
|
+
|
26
|
+
# Creates a block with the given name, body, and location. If no
|
27
|
+
# location is given, it is assumed from the name and body.
|
28
|
+
#
|
29
|
+
# @param name [Scanner::Token] The name of the block.
|
30
|
+
# @param body [Node::Root] The body of the block.
|
31
|
+
|
32
|
+
# @param location [Location] The location of the block.
|
33
|
+
def initialize(name:, body:, arguments: nil, pairs: nil, location: nil)
|
34
|
+
if !location && !arguments
|
35
|
+
fail ArgumentError, "Expected either a location or " \
|
36
|
+
"arguments, got neither"
|
37
|
+
end
|
38
|
+
|
39
|
+
@name = name.is_a?(Yoga::Token) ? name.value : name
|
40
|
+
@body = body
|
41
|
+
@location = location || arguments.map(&:location)
|
42
|
+
.inject(name.location.union(body.location),
|
43
|
+
:union)
|
44
|
+
@pairs = pairs.freeze || derive_pairs(arguments).freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pretty inspect.
|
48
|
+
#
|
49
|
+
# @return [::String]
|
50
|
+
def inspect
|
51
|
+
"#<#{self.class} name=#{@name.inspect} pairs=#{@pairs.inspect} " \
|
52
|
+
"body=#{@body.inspect} location=#{@location.inspect}>"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Equates this object with another object. If the other object is
|
56
|
+
# this object, it returns true; otherwise, if it is a block, whose
|
57
|
+
# properties all equal this one, it returns true; otherwise, it
|
58
|
+
# returns false.
|
59
|
+
#
|
60
|
+
# @param other [::Object]
|
61
|
+
# @return [Boolean]
|
62
|
+
def ==(other)
|
63
|
+
equal?(other) || other.is_a?(Block) && @name == other.name &&
|
64
|
+
@body == other.body && @location == other.location
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def update_name(name)
|
70
|
+
Block.new(name: name, body: @body, pairs: @pairs, location: @location)
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_body(body)
|
74
|
+
Block.new(name: @name, body: body, pairs: @pairs, location: @location)
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_pairs(pairs)
|
78
|
+
Block.new(name: @name, body: @body, pairs: pairs, location: @location)
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_arguments(arguments)
|
82
|
+
update_pairs(derive_pairs(arguments))
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_location(location)
|
86
|
+
Block.new(name: @name, body: @body, pairs: @pairs, location: location)
|
87
|
+
end
|
88
|
+
|
89
|
+
def derive_pairs(arguments)
|
90
|
+
arguments.inject({}) do |a, pair|
|
91
|
+
k, v = pair.key, pair.value
|
92
|
+
a.merge!(k.value => v.value)
|
93
|
+
end.freeze
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Brandish
|
5
|
+
class Parser
|
6
|
+
class Node
|
7
|
+
# A command node. This takes the form of `{.a b=c}`, and executes a
|
8
|
+
# given command with the given options.
|
9
|
+
class Command < Node
|
10
|
+
# The name of the command node. This is the name of the command that
|
11
|
+
# is executed.
|
12
|
+
#
|
13
|
+
# @return [::String]
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# The "pairs" of arguments to be passed to the command.
|
17
|
+
#
|
18
|
+
# @return [{::String => ::String}]
|
19
|
+
attr_reader :pairs
|
20
|
+
|
21
|
+
# Initialize the command node.
|
22
|
+
#
|
23
|
+
# @overload initialize(name:, arguments:, location: nil)
|
24
|
+
# Creates a command node with the given name. The arguments are
|
25
|
+
# processed to turn into a hash that is a key-value store of the
|
26
|
+
# arguments; if there are duplicate key-values pairs, the earlier
|
27
|
+
# ones are overwritten. If no location is provided, it is assumed
|
28
|
+
# from the name and arguments.
|
29
|
+
#
|
30
|
+
# @param name [::String, Scanner::Token] The name of the command. If
|
31
|
+
# no location is provided, this *must* respond to `#location`.
|
32
|
+
# @param arguments [<Node::Pair>] The pairs of arguments as an array
|
33
|
+
# of nodes.
|
34
|
+
# @overload initialize(name:, pairs:, location:)
|
35
|
+
# Creates a command node with the given name and argument pairs.
|
36
|
+
# Since no location can be derived from either, the location is
|
37
|
+
# required.
|
38
|
+
#
|
39
|
+
# @param name [::String, Scanner::Token] The name of the command.
|
40
|
+
# @param pairs [{::String => ::String, ::Numeric}] The argument pairs
|
41
|
+
# for the command.
|
42
|
+
# @param location [Location] The location of the node.
|
43
|
+
def initialize(name:, arguments: nil, pairs: nil, location: nil)
|
44
|
+
if !location && !arguments
|
45
|
+
fail ArgumentError, "Expected either a location or " \
|
46
|
+
"arguments, got neither"
|
47
|
+
end
|
48
|
+
|
49
|
+
@name = name.is_a?(Yoga::Token) ? name.value : name.freeze
|
50
|
+
@location = location || arguments.map(&:location)
|
51
|
+
.inject(name.location, :union)
|
52
|
+
@pairs = pairs.freeze || derive_pairs(arguments).freeze
|
53
|
+
end
|
54
|
+
|
55
|
+
# Determines if this object and the given object are equal. If the
|
56
|
+
# other object is this object, it returns true; otherwise, if the other
|
57
|
+
# object is a {Command}, and all of the properties of that object equal
|
58
|
+
# this one, then it returns true; otherwise, it returns false.
|
59
|
+
#
|
60
|
+
# @param other [::Object] The object to equal.
|
61
|
+
# @return [Boolean]
|
62
|
+
def ==(other)
|
63
|
+
equal?(other) || (other.is_a?(Command) && @name == other.name &&
|
64
|
+
@location == other.location && @pairs == other.pairs)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Pretty inspect.
|
68
|
+
#
|
69
|
+
# @return [::String]
|
70
|
+
def inspect
|
71
|
+
"#<#{self.class} name=#{@name.inspect} pairs=#{@pairs.inspect} " \
|
72
|
+
"location=#{@location.inspect}>"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def update_name(name)
|
78
|
+
Command.new(name: name, pairs: @pairs, location: @location)
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_pairs(pairs)
|
82
|
+
Command.new(name: @name, pairs: pairs, location: @location)
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_arguments(arguments)
|
86
|
+
update_pairs(derive_pairs(arguments))
|
87
|
+
end
|
88
|
+
|
89
|
+
def update_location(location)
|
90
|
+
Command.new(name: @name, pairs: @pairs, location: location)
|
91
|
+
end
|
92
|
+
|
93
|
+
def derive_pairs(arguments)
|
94
|
+
arguments.inject({}) do |a, pair|
|
95
|
+
k, v = pair.key, pair.value
|
96
|
+
a.merge!(k.value => v.value)
|
97
|
+
end.freeze
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Brandish
|
5
|
+
class Parser
|
6
|
+
class Node
|
7
|
+
# A key-value pair used for a command. This is used to represent an
|
8
|
+
# argument for the command node.
|
9
|
+
class Pair < Node
|
10
|
+
# The key of the pair. This will *always* be a `:TEXT`.
|
11
|
+
#
|
12
|
+
# @return [Scanner::Token] The key.
|
13
|
+
attr_reader :key
|
14
|
+
|
15
|
+
# The value of the pair.
|
16
|
+
#
|
17
|
+
# @return [Parser::Node::Text, Parser::Node::String] The value.
|
18
|
+
attr_reader :value
|
19
|
+
|
20
|
+
# Initialize the pair node with the given key, value and location.
|
21
|
+
#
|
22
|
+
# @param key [Yoga::Token] The key.
|
23
|
+
# @param value [Parser::Node::Text, Parser::Node::String] The value.
|
24
|
+
# @param location [Location] The location of the key-value pair.
|
25
|
+
def initialize(key:, value:, location: nil)
|
26
|
+
@key = key
|
27
|
+
@value = value
|
28
|
+
@location = location || key.location.union(value.location)
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
# Pretty inspect.
|
33
|
+
#
|
34
|
+
# @return [::String]
|
35
|
+
def inspect
|
36
|
+
"#<#{self.class} key=#{@key.inspect} value=#{@value.inspect} " \
|
37
|
+
"location=#{@location.inspect}>"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Brandish
|
5
|
+
class Parser
|
6
|
+
class Node
|
7
|
+
# The root node. This is the parent of the full document.
|
8
|
+
class Root < Node
|
9
|
+
# The children of the root node. This should be a series of nodes
|
10
|
+
# that are in the body.
|
11
|
+
#
|
12
|
+
# @return [<Node>]
|
13
|
+
attr_reader :children
|
14
|
+
|
15
|
+
# Initialize the root node with the given children and location.
|
16
|
+
#
|
17
|
+
# @param children [<Node>] The children of the root node. See
|
18
|
+
# {#children}.
|
19
|
+
# @param location [Location] The location of the node. If no location
|
20
|
+
# is provided, it assumed from the children.
|
21
|
+
def initialize(children:, location: nil)
|
22
|
+
@children = children.freeze
|
23
|
+
@location = location || derive_location(children)
|
24
|
+
fail unless children.any?
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# Pretty inspect.
|
29
|
+
#
|
30
|
+
# @return [::String]
|
31
|
+
def inspect
|
32
|
+
"#<#{self.class} children=#{@children.inspect} " \
|
33
|
+
"location=#{@location.inspect}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks for equivalence between this and another object. If the other
|
37
|
+
# object is this object, it returns true; otherwise, if the other
|
38
|
+
# object is a root, and its properties are equal to this one, it returns
|
39
|
+
# true; otherwise, it returns false.
|
40
|
+
#
|
41
|
+
# @param other [::Object]
|
42
|
+
# @return [Boolean]
|
43
|
+
def ==(other)
|
44
|
+
equal?(other) || other.is_a?(Root) && @children == other.children &&
|
45
|
+
@location == other.location
|
46
|
+
end
|
47
|
+
|
48
|
+
# This flattens out the root node into a single string value. This is
|
49
|
+
# used for outputing the contents.
|
50
|
+
#
|
51
|
+
# @raise [NodeError] if the node contains a non-root or non-text node.
|
52
|
+
# @return [::String]
|
53
|
+
def flatten
|
54
|
+
children.map do |child|
|
55
|
+
case child
|
56
|
+
when Node::Root then child.flatten
|
57
|
+
when Node::Text then child.value
|
58
|
+
else
|
59
|
+
fail NodeError.new("Unexpected node `#{child.class}",
|
60
|
+
node.location)
|
61
|
+
end
|
62
|
+
end.join
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def derive_location(children)
|
68
|
+
children.map(&:location).inject(:union) || Yoga::Location.default
|
69
|
+
rescue
|
70
|
+
Yoga::Location.default
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_children(children)
|
74
|
+
Root.new(children: children, location: @location)
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_location(location)
|
78
|
+
Root.new(children: @children, location: location)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Brandish
|
5
|
+
class Parser
|
6
|
+
class Node
|
7
|
+
# A "string." Because of how loose the language is, this is similar to
|
8
|
+
# a {Text} node, but has no restriction on the allowed values for the
|
9
|
+
# node.
|
10
|
+
class String < Text
|
11
|
+
# A set of tokens kinds that are allowed to be in a string node.
|
12
|
+
#
|
13
|
+
# @return [::Set<::Symbol>]
|
14
|
+
TOKENS = (Node::Text::TOKENS - ::Set[:'"']) + ::Set[:<, :>]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
module Brandish
|
7
|
+
class Parser
|
8
|
+
class Node
|
9
|
+
# A text node. This contains only text, and has no other node
|
10
|
+
# decendants. This node automatically merges the contents of the text
|
11
|
+
# into a single value, and places it in the `value` attribute.
|
12
|
+
class Text < Node
|
13
|
+
# A set of tokens kinds that are allowed to be in a text node.
|
14
|
+
#
|
15
|
+
# @return [::Set<::Symbol>]
|
16
|
+
TOKENS =
|
17
|
+
::Set[:SPACE, :TEXT, :LINE, :NUMERIC, :ESCAPE, :/, :'"', :'='].freeze
|
18
|
+
|
19
|
+
# The value of the text node. This is the string value of the source
|
20
|
+
# text that this node is based off of.
|
21
|
+
#
|
22
|
+
# @return [::String]
|
23
|
+
attr_reader :value
|
24
|
+
|
25
|
+
# Initialize the node.
|
26
|
+
#
|
27
|
+
# @overload initialize(tokens:, location: nil)
|
28
|
+
# Initialize the text node with the given series of tokens. The
|
29
|
+
# location can be either provided, or assumed from the locations of
|
30
|
+
# the tokens. The tokens are concatenated into a single string,
|
31
|
+
# and used to provide the text.
|
32
|
+
#
|
33
|
+
# @param tokens [<Scanner::Token>] The tokens that make up
|
34
|
+
# this text node.
|
35
|
+
# @param location [Location] The location of the text node. If none
|
36
|
+
# is provided, it is assumed from the tokens.
|
37
|
+
# @overload initialize(value:, location:)
|
38
|
+
# Initialize the text node with the given value. The location must
|
39
|
+
# be provided.
|
40
|
+
#
|
41
|
+
# @param value [::String] The value of the text node.
|
42
|
+
# @param location [Location] The location of the text node.
|
43
|
+
def initialize(tokens: nil, value: nil, location: nil)
|
44
|
+
unless tokens || (value && location)
|
45
|
+
fail ::ArgumentError, "Expected either a set of tokens or a " \
|
46
|
+
"value and a location, got neither"
|
47
|
+
end
|
48
|
+
|
49
|
+
@value = value || derive_value(tokens)
|
50
|
+
@location = location || derive_location(tokens)
|
51
|
+
freeze
|
52
|
+
end
|
53
|
+
|
54
|
+
# Pretty inspect.
|
55
|
+
#
|
56
|
+
# @return [::String]
|
57
|
+
def inspect
|
58
|
+
"#<#{self.class} value=#{@value.inspect} " \
|
59
|
+
"location=#{@location.inspect}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Determines if this object equals another object. If the other object
|
63
|
+
# is this object, it returns true; otherwise, if the other object is a
|
64
|
+
# {Text}, and all of the properties are equal, it returns true;
|
65
|
+
# otherwise, it returns false.
|
66
|
+
#
|
67
|
+
# @param other [::Object]
|
68
|
+
# @return [Boolean]
|
69
|
+
def ==(other)
|
70
|
+
equal?(other) || other.is_a?(self.class) && @value == other.value &&
|
71
|
+
@location == other.location
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def update_value(value)
|
77
|
+
Text.new(value: value, location: @location)
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_tokens(tokens)
|
81
|
+
update_value(derive_value(tokens))
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_location(location)
|
85
|
+
Text.new(value: @value, location: location)
|
86
|
+
end
|
87
|
+
|
88
|
+
def derive_location(tokens)
|
89
|
+
unless tokens
|
90
|
+
fail ::ArgumentError, "Expected either location or tokens, got" \
|
91
|
+
" neither"
|
92
|
+
end
|
93
|
+
|
94
|
+
tokens.map(&:location).inject(:union)
|
95
|
+
end
|
96
|
+
|
97
|
+
def derive_value(tokens)
|
98
|
+
assert_valid_tokens(tokens)
|
99
|
+
tokens
|
100
|
+
.map { |t| t.kind == :ESCAPE ? t.value[-1] : t.value }
|
101
|
+
.join
|
102
|
+
.freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
def assert_valid_tokens(tokens)
|
106
|
+
valid = tokens.all? { |t| TOKENS.include?(t.kind) }
|
107
|
+
return valid if valid
|
108
|
+
fail ::ArgumentError, "Expected tokens to all be one of " \
|
109
|
+
"#{TOKENS.map(&:inspect).join(', ')}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|