rake-pipeline-fork 0.8.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 +15 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +12 -0
- data/.yardopts +2 -0
- data/GETTING_STARTED.md +268 -0
- data/Gemfile +14 -0
- data/LICENSE +20 -0
- data/README.markdown +11 -0
- data/README.yard +178 -0
- data/Rakefile +21 -0
- data/bin/rakep +4 -0
- data/examples/copying_files.md +12 -0
- data/examples/minifying_files.md +37 -0
- data/examples/modifying_pipelines.md +67 -0
- data/examples/multiple_pipelines.md +77 -0
- data/lib/generators/rake/pipeline/install/install_generator.rb +70 -0
- data/lib/rake-pipeline.rb +462 -0
- data/lib/rake-pipeline/cli.rb +56 -0
- data/lib/rake-pipeline/dsl.rb +9 -0
- data/lib/rake-pipeline/dsl/pipeline_dsl.rb +246 -0
- data/lib/rake-pipeline/dsl/project_dsl.rb +108 -0
- data/lib/rake-pipeline/dynamic_file_task.rb +194 -0
- data/lib/rake-pipeline/error.rb +17 -0
- data/lib/rake-pipeline/file_wrapper.rb +182 -0
- data/lib/rake-pipeline/filter.rb +249 -0
- data/lib/rake-pipeline/filters.rb +4 -0
- data/lib/rake-pipeline/filters/concat_filter.rb +63 -0
- data/lib/rake-pipeline/filters/gsub_filter.rb +56 -0
- data/lib/rake-pipeline/filters/ordering_concat_filter.rb +38 -0
- data/lib/rake-pipeline/filters/pipeline_finalizing_filter.rb +21 -0
- data/lib/rake-pipeline/graph.rb +178 -0
- data/lib/rake-pipeline/manifest.rb +86 -0
- data/lib/rake-pipeline/manifest_entry.rb +34 -0
- data/lib/rake-pipeline/matcher.rb +141 -0
- data/lib/rake-pipeline/middleware.rb +72 -0
- data/lib/rake-pipeline/precompile.rake +8 -0
- data/lib/rake-pipeline/project.rb +335 -0
- data/lib/rake-pipeline/rails_plugin.rb +10 -0
- data/lib/rake-pipeline/railtie.rb +34 -0
- data/lib/rake-pipeline/reject_matcher.rb +29 -0
- data/lib/rake-pipeline/server.rb +15 -0
- data/lib/rake-pipeline/sorted_pipeline.rb +19 -0
- data/lib/rake-pipeline/version.rb +6 -0
- data/rails/init.rb +2 -0
- data/rake-pipeline.gemspec +24 -0
- data/spec/cli_spec.rb +71 -0
- data/spec/concat_filter_spec.rb +37 -0
- data/spec/dsl/pipeline_dsl_spec.rb +165 -0
- data/spec/dsl/project_dsl_spec.rb +41 -0
- data/spec/dynamic_file_task_spec.rb +119 -0
- data/spec/encoding_spec.rb +106 -0
- data/spec/file_wrapper_spec.rb +132 -0
- data/spec/filter_spec.rb +332 -0
- data/spec/graph_spec.rb +56 -0
- data/spec/gsub_filter_spec.rb +87 -0
- data/spec/manifest_entry_spec.rb +46 -0
- data/spec/manifest_spec.rb +67 -0
- data/spec/matcher_spec.rb +141 -0
- data/spec/middleware_spec.rb +199 -0
- data/spec/ordering_concat_filter_spec.rb +42 -0
- data/spec/pipeline_spec.rb +232 -0
- data/spec/project_spec.rb +295 -0
- data/spec/rake_acceptance_spec.rb +738 -0
- data/spec/rake_tasks_spec.rb +21 -0
- data/spec/reject_matcher_spec.rb +31 -0
- data/spec/sorted_pipeline_spec.rb +27 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/spec_helpers/file_utils.rb +35 -0
- data/spec/support/spec_helpers/filters.rb +37 -0
- data/spec/support/spec_helpers/input_helpers.rb +23 -0
- data/spec/support/spec_helpers/memory_file_wrapper.rb +31 -0
- data/spec/support/spec_helpers/memory_manifest.rb +19 -0
- data/tools/perfs +101 -0
- metadata +215 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rake
|
2
|
+
class Pipeline
|
3
|
+
# The general Rake::Pipeline error class
|
4
|
+
class Error < ::StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# The error that Rake::Pipeline uses when it detects
|
8
|
+
# that a file uses an improper encoding.
|
9
|
+
class EncodingError < Error
|
10
|
+
end
|
11
|
+
|
12
|
+
# The error that Rake::Pipeline uses if you try to
|
13
|
+
# write to a FileWrapper before creating it.
|
14
|
+
class UnopenedFile < Error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Rake
|
2
|
+
class Pipeline
|
3
|
+
# This class wraps a file for consumption inside of filters. It is
|
4
|
+
# initialized with a root and path, and filters usually use the
|
5
|
+
# {#read} and {#write} methods to work with these files.
|
6
|
+
#
|
7
|
+
# The {#root} and +path+ parameters are provided by the {Filter}
|
8
|
+
# class' internal implementation. Individual filters do not need
|
9
|
+
# to worry about them.
|
10
|
+
#
|
11
|
+
# The root of a {FileWrapper} is always an absolute path.
|
12
|
+
class FileWrapper
|
13
|
+
# @return [String] an absolute path representing this {FileWrapper}'s
|
14
|
+
# root directory.
|
15
|
+
attr_accessor :root
|
16
|
+
|
17
|
+
# @return [String] the path to the file represented by the {FileWrapper},
|
18
|
+
# relative to its {#root}.
|
19
|
+
attr_accessor :path
|
20
|
+
|
21
|
+
# @return [String] the encoding that the file represented by this
|
22
|
+
# {FileWrapper} is encoded in. Filters set the {#encoding} to
|
23
|
+
# +BINARY+ if they are declared as processing binary data.
|
24
|
+
attr_accessor :encoding
|
25
|
+
|
26
|
+
# Create a new {FileWrapper}, passing in optional root, path, and
|
27
|
+
# encoding. Any of the parameters can be ommitted and supplied later.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def initialize(root=nil, path=nil, encoding="UTF-8")
|
31
|
+
@root, @path, @encoding = root, path, encoding
|
32
|
+
@created_file = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a new {FileWrapper FileWrapper} with the same root and
|
36
|
+
# path as this {FileWrapper FileWrapper}, but with a specified
|
37
|
+
# encoding.
|
38
|
+
#
|
39
|
+
# @param [String] encoding the encoding for the new object
|
40
|
+
# @return [FileWrapper]
|
41
|
+
def with_encoding(encoding)
|
42
|
+
self.class.new(@root, @path, encoding)
|
43
|
+
end
|
44
|
+
|
45
|
+
# A {FileWrapper} is equal to another {FileWrapper} for hashing purposes
|
46
|
+
# if they have the same {#root} and {#path}
|
47
|
+
#
|
48
|
+
# @param [FileWrapper] other another {FileWrapper} to compare.
|
49
|
+
# @return [true,false]
|
50
|
+
def eql?(other)
|
51
|
+
return false unless other.is_a?(self.class)
|
52
|
+
root == other.root && path == other.path
|
53
|
+
end
|
54
|
+
alias == eql?
|
55
|
+
|
56
|
+
# Similar to {#eql?}, generate a {FileWrapper}'s {#hash} from its {#root}
|
57
|
+
# and {#path}
|
58
|
+
#
|
59
|
+
# @see #eql?
|
60
|
+
# @return [Fixnum] a hash code
|
61
|
+
def hash
|
62
|
+
[root, path].hash
|
63
|
+
end
|
64
|
+
|
65
|
+
# The full path of a FileWrapper is its root joined with its path
|
66
|
+
#
|
67
|
+
# @return [String] the {FileWrapper}'s full path
|
68
|
+
def fullpath
|
69
|
+
raise "#{root}, #{path}" unless root =~ /^(\/|[a-zA-Z]:[\\\/])/
|
70
|
+
File.join(root, path)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check to see if this file is inside the given directory
|
74
|
+
#
|
75
|
+
# @return [Boolean]
|
76
|
+
def in_directory?(directory)
|
77
|
+
!!(fullpath =~ %r{^#{Regexp.escape(directory)}/})
|
78
|
+
end
|
79
|
+
|
80
|
+
# Make FileWrappers sortable
|
81
|
+
#
|
82
|
+
# @param [FileWrapper] other {FileWrapper FileWrapper}
|
83
|
+
# @return [Fixnum] -1, 0, or 1
|
84
|
+
def <=>(other)
|
85
|
+
[root, path, encoding] <=> [other.root, other.path, other.encoding]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Does the file represented by the {FileWrapper} exist in the file system?
|
89
|
+
#
|
90
|
+
# @return [true,false]
|
91
|
+
def exists?
|
92
|
+
File.exists?(fullpath)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Read the contents of the file represented by the {FileWrapper}.
|
96
|
+
#
|
97
|
+
# Read the file using the {FileWrapper}'s encoding, which will result in
|
98
|
+
# this method returning a +String+ tagged with the {FileWrapper}'s encoding.
|
99
|
+
#
|
100
|
+
# @return [String] the contents of the file
|
101
|
+
# @raise [EncodingError] when the contents of the file are not valid in the
|
102
|
+
# expected encoding specified in {#encoding}.
|
103
|
+
def read
|
104
|
+
contents = if "".respond_to?(:encode)
|
105
|
+
File.read(fullpath, :encoding => encoding)
|
106
|
+
else
|
107
|
+
File.read(fullpath)
|
108
|
+
end
|
109
|
+
|
110
|
+
# In our unit tests Rubinius returns false when the encoding is BINARY
|
111
|
+
# The encoding type check bypasses the problem and is probably acceptable, but isn't ideal
|
112
|
+
if encoding != "BINARY" && "".respond_to?(:encode) && !contents.valid_encoding?
|
113
|
+
raise EncodingError, "The file at the path #{fullpath} is not valid #{encoding}. Please save it again as #{encoding}."
|
114
|
+
end
|
115
|
+
|
116
|
+
contents
|
117
|
+
end
|
118
|
+
|
119
|
+
# Create a new file at the {FileWrapper}'s {#fullpath}. If the file already
|
120
|
+
# exists, it will be overwritten.
|
121
|
+
#
|
122
|
+
# @api private
|
123
|
+
# @yieldparam [File] file the newly created file
|
124
|
+
# @return [File] if a block was not given
|
125
|
+
def create
|
126
|
+
FileUtils.mkdir_p(File.dirname(fullpath))
|
127
|
+
|
128
|
+
@created_file = if "".respond_to?(:encode)
|
129
|
+
File.open(fullpath, "w:#{encoding}")
|
130
|
+
else
|
131
|
+
File.open(fullpath, "w")
|
132
|
+
end
|
133
|
+
|
134
|
+
if block_given?
|
135
|
+
yield @created_file
|
136
|
+
end
|
137
|
+
|
138
|
+
@created_file
|
139
|
+
ensure
|
140
|
+
if block_given?
|
141
|
+
@created_file.close
|
142
|
+
@created_file = nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Close the file represented by the {FileWrapper} if it was previously opened.
|
147
|
+
#
|
148
|
+
# @api private
|
149
|
+
# @return [void]
|
150
|
+
def close
|
151
|
+
raise IOError, "closed stream" unless @created_file
|
152
|
+
@created_file.close
|
153
|
+
@created_file = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
# Check to see whether the file represented by the {FileWrapper} is open.
|
157
|
+
#
|
158
|
+
# @api private
|
159
|
+
# @return [true,false]
|
160
|
+
def closed?
|
161
|
+
@created_file.nil?
|
162
|
+
end
|
163
|
+
|
164
|
+
# Write a String to a previously opened file. This method is called repeatedly
|
165
|
+
# by a {Filter}'s +#generate_output+ method and does not create a brand new
|
166
|
+
# file for each invocation.
|
167
|
+
#
|
168
|
+
# @raise [UnopenedFile] if the file is not already opened.
|
169
|
+
def write(string)
|
170
|
+
raise UnopenedFile unless @created_file
|
171
|
+
@created_file.write(string)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [String] A pretty representation of the {FileWrapper}.
|
175
|
+
def inspect
|
176
|
+
"#<FileWrapper root=#{root.inspect} path=#{path.inspect} encoding=#{encoding.inspect}>"
|
177
|
+
end
|
178
|
+
|
179
|
+
alias to_s inspect
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require "rake"
|
2
|
+
|
3
|
+
module Rake
|
4
|
+
class Pipeline
|
5
|
+
|
6
|
+
# A Filter is added to a pipeline and converts input files
|
7
|
+
# into output files.
|
8
|
+
#
|
9
|
+
# Filters operate on FileWrappers, which abstract away the
|
10
|
+
# root directory of a file, providing a relative path and
|
11
|
+
# a mechanism for reading and writing.
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# For instance, a filter to wrap the contents of each file
|
15
|
+
# in a JavaScript closure would look like:
|
16
|
+
#
|
17
|
+
# !!!ruby
|
18
|
+
# require "json"
|
19
|
+
#
|
20
|
+
# class ClosureFilter < Rake::Pipeline::Filter
|
21
|
+
# def generate_output(inputs, output)
|
22
|
+
# inputs.each do |input|
|
23
|
+
# output.write "(function() { #{input.read.to_json} })()"
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# A filter's files come from the input directory or the directory
|
29
|
+
# owned by the previous filter, but filters are insulated from
|
30
|
+
# this concern.
|
31
|
+
#
|
32
|
+
# You can call +path+ on a FileWrapper to get the file's relative
|
33
|
+
# path, or `fullpath` to get its absolute path, but you should,
|
34
|
+
# in general, not use `fullpath` but instead use methods of
|
35
|
+
# FileWrapper like `read` and `write` that abstract the details
|
36
|
+
# from you.
|
37
|
+
#
|
38
|
+
# @see ConcatFilter Rake::Pipeline::ConcatFilter for another
|
39
|
+
# example filter implementation.
|
40
|
+
#
|
41
|
+
# @abstract
|
42
|
+
class Filter
|
43
|
+
# @return [Array<FileWrapper>] an Array of FileWrappers that
|
44
|
+
# represent the inputs of this filter. The Pipeline will
|
45
|
+
# usually set this up.
|
46
|
+
attr_accessor :input_files
|
47
|
+
|
48
|
+
# @return [Proc] a block that returns the relative output
|
49
|
+
# filename for a particular input file. If the block accepts
|
50
|
+
# just one argument, it will be passed the input's path. If
|
51
|
+
# it accepts two, it will also be passed the input itself.
|
52
|
+
attr_accessor :output_name_generator
|
53
|
+
|
54
|
+
# @return [String] the root directory to write output files
|
55
|
+
# to. For the last filter in a pipeline, the pipeline will
|
56
|
+
# set this to the pipeline's output. For all other filters,
|
57
|
+
# the pipeline will create a temporary directory that it
|
58
|
+
# also uses when creating FileWrappers for the next filter's
|
59
|
+
# inputs.
|
60
|
+
attr_accessor :output_root
|
61
|
+
|
62
|
+
# @return [Array<Rake::Task>] an Array of Rake tasks created
|
63
|
+
# for this filter. Each unique output file will get a
|
64
|
+
# single task.
|
65
|
+
attr_reader :rake_tasks
|
66
|
+
|
67
|
+
# @return [Rake::Application] the Rake::Application that the
|
68
|
+
# filter should define new rake tasks on.
|
69
|
+
attr_writer :rake_application
|
70
|
+
|
71
|
+
# @return [Rake::Pipeline] the Rake::Pipeline that contains
|
72
|
+
# this filter.
|
73
|
+
attr_accessor :pipeline
|
74
|
+
|
75
|
+
attr_writer :manifest, :last_manifest
|
76
|
+
|
77
|
+
attr_writer :file_wrapper_class
|
78
|
+
|
79
|
+
# @param [Proc] block a block to use as the Filter's
|
80
|
+
# {#output_name_generator}.
|
81
|
+
def initialize(&block)
|
82
|
+
block ||= proc { |input| input }
|
83
|
+
@output_name_generator = block
|
84
|
+
@input_files = []
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Rake::Pipeline::Manifest] the manifest passed
|
88
|
+
# to generated rake tasks. Use the pipeline's manifest
|
89
|
+
# if this is not set
|
90
|
+
def manifest
|
91
|
+
@manifest || pipeline.manifest
|
92
|
+
end
|
93
|
+
|
94
|
+
def last_manifest
|
95
|
+
@last_manifest || pipeline.last_manifest
|
96
|
+
end
|
97
|
+
|
98
|
+
# Invoke this method in a subclass of Filter to declare that
|
99
|
+
# it expects to work with BINARY data, and that data that is
|
100
|
+
# not valid UTF-8 should be allowed.
|
101
|
+
#
|
102
|
+
# @return [void]
|
103
|
+
def self.processes_binary_files
|
104
|
+
define_method(:encoding) { "BINARY" }
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Class] the class to use as the wrapper for output
|
108
|
+
# files.
|
109
|
+
def file_wrapper_class
|
110
|
+
@file_wrapper_class ||= FileWrapper
|
111
|
+
end
|
112
|
+
|
113
|
+
# Set the input files to a list of FileWrappers. The filter
|
114
|
+
# will map these into equivalent FileWrappers with the
|
115
|
+
# filter's encoding applied.
|
116
|
+
#
|
117
|
+
# By default, a filter's encoding is +UTF-8+, unless
|
118
|
+
# it calls #processes_binary_files, which changes it to
|
119
|
+
# +BINARY+.
|
120
|
+
#
|
121
|
+
# @param [Array<FileWrapper>] a list of FileWrapper objects
|
122
|
+
def input_files=(files)
|
123
|
+
@input_files = files.map do |file|
|
124
|
+
file.with_encoding(encoding)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# A hash of output files pointing at their associated input
|
129
|
+
# files. The output names are created by applying the
|
130
|
+
# {#output_name_generator} to each input file.
|
131
|
+
#
|
132
|
+
# For exmaple, if you had the following input files:
|
133
|
+
#
|
134
|
+
# javascripts/jquery.js
|
135
|
+
# javascripts/sproutcore.js
|
136
|
+
# stylesheets/sproutcore.css
|
137
|
+
#
|
138
|
+
# And you had the following {#output_name_generator}:
|
139
|
+
#
|
140
|
+
# !!!ruby
|
141
|
+
# filter.output_name_generator = proc do |filename|
|
142
|
+
# # javascripts/jquery.js becomes:
|
143
|
+
# # ["javascripts", "jquery", "js"]
|
144
|
+
# directory, file, ext = file.split(/[\.\/]/)
|
145
|
+
#
|
146
|
+
# "#{directory}.#{ext}"
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# You would end up with the following hash:
|
150
|
+
#
|
151
|
+
# !!!ruby
|
152
|
+
# {
|
153
|
+
# #<FileWrapper path="javascripts.js" root="#{output_root}> => [
|
154
|
+
# #<FileWrapper path="javascripts/jquery.js" root="#{previous_filter.output_root}">,
|
155
|
+
# #<FileWrapper path="javascripts/sproutcore.js" root="#{previous_filter.output_root}">
|
156
|
+
# ],
|
157
|
+
# #<FileWrapper path="stylesheets.css" root="#{output_root}"> => [
|
158
|
+
# #<FileWrapper path="stylesheets/sproutcore.css" root=#{previous_filter.output_root}">
|
159
|
+
# ]
|
160
|
+
# }
|
161
|
+
#
|
162
|
+
# Each output file becomes a Rake task, which invokes the +#generate_output+
|
163
|
+
# method defined by the subclass of {Filter} with the Array of inputs and
|
164
|
+
# the output (all as {FileWrapper}s).
|
165
|
+
#
|
166
|
+
# @return [Hash{FileWrapper => Array<FileWrapper>}]
|
167
|
+
def outputs
|
168
|
+
hash = {}
|
169
|
+
|
170
|
+
input_files.each do |file|
|
171
|
+
output_wrappers(file).each do |output|
|
172
|
+
hash[output] ||= []
|
173
|
+
hash[output] << file
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
hash
|
178
|
+
end
|
179
|
+
|
180
|
+
# An Array of the {FileWrapper} objects that rerepresent this filter's
|
181
|
+
# output files. It is the same as +outputs.keys+.
|
182
|
+
#
|
183
|
+
# @see #outputs
|
184
|
+
# @return [Array<FileWrapper>]
|
185
|
+
def output_files
|
186
|
+
input_files.map { |file| output_wrappers(file) }.flatten.uniq
|
187
|
+
end
|
188
|
+
|
189
|
+
# The Rake::Application that the filter should define new tasks on.
|
190
|
+
#
|
191
|
+
# @return [Rake::Application]
|
192
|
+
def rake_application
|
193
|
+
@rake_application || Rake.application
|
194
|
+
end
|
195
|
+
|
196
|
+
# @param [FileWrapper] file wrapper to get paths for
|
197
|
+
# @return [Array<String>] array of file paths within additional dependencies
|
198
|
+
def additional_dependencies(input)
|
199
|
+
[]
|
200
|
+
end
|
201
|
+
|
202
|
+
# Generate the Rake tasks for the output files of this filter.
|
203
|
+
#
|
204
|
+
# @see #outputs #outputs (for information on how the output files are determined)
|
205
|
+
# @return [void]
|
206
|
+
def generate_rake_tasks
|
207
|
+
@rake_tasks = outputs.map do |output, inputs|
|
208
|
+
additional_paths = []
|
209
|
+
inputs.each do |input|
|
210
|
+
|
211
|
+
create_file_task(input.fullpath).dynamic do
|
212
|
+
additional_paths += additional_dependencies(input)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
additional_paths.each { |path| create_file_task(path) }
|
216
|
+
|
217
|
+
create_file_task(output.fullpath, inputs.map(&:fullpath)) do
|
218
|
+
output.create { generate_output(inputs, output) }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
# @attr_reader
|
225
|
+
def encoding
|
226
|
+
"UTF-8"
|
227
|
+
end
|
228
|
+
|
229
|
+
def create_file_task(output, deps=[], &block)
|
230
|
+
task = rake_application.define_task(Rake::Pipeline::DynamicFileTask, output => deps, &block)
|
231
|
+
task.manifest = manifest
|
232
|
+
task.last_manifest = last_manifest
|
233
|
+
task
|
234
|
+
end
|
235
|
+
|
236
|
+
def output_wrappers(input)
|
237
|
+
output_paths(input).map do |path|
|
238
|
+
file_wrapper_class.new(output_root, path, encoding)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def output_paths(input)
|
243
|
+
args = [ input.path ]
|
244
|
+
args << input if output_name_generator.arity == 2
|
245
|
+
Array(output_name_generator.call(*args))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|