jekyll-minibundle 1.5.1 → 1.6.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 +4 -4
- data/CHANGELOG.md +18 -6
- data/README.md +34 -24
- data/Rakefile +19 -14
- data/jekyll-minibundle.gemspec +5 -4
- data/lib/jekyll/minibundle/asset_bundle.rb +24 -4
- data/lib/jekyll/minibundle/asset_file_operations.rb +0 -8
- data/lib/jekyll/minibundle/asset_file_properties.rb +26 -8
- data/lib/jekyll/minibundle/asset_file_registry.rb +57 -21
- data/lib/jekyll/minibundle/asset_tag_markup.rb +13 -4
- data/lib/jekyll/minibundle/bundle_file.rb +9 -6
- data/lib/jekyll/minibundle/compatibility.rb +11 -1
- data/lib/jekyll/minibundle/development_file.rb +6 -0
- data/lib/jekyll/minibundle/development_file_collection.rb +3 -16
- data/lib/jekyll/minibundle/environment.rb +5 -12
- data/lib/jekyll/minibundle/files.rb +18 -0
- data/lib/jekyll/minibundle/hashes.rb +20 -0
- data/lib/jekyll/minibundle/mini_bundle_block.rb +52 -12
- data/lib/jekyll/minibundle/mini_stamp_tag.rb +27 -8
- data/lib/jekyll/minibundle/stamp_file.rb +9 -6
- data/lib/jekyll/minibundle/version.rb +1 -1
- data/test/fixture/site/_bin/with_count +6 -5
- data/test/integration/known_caveats_test.rb +89 -0
- data/test/integration/minibundle_development_mode_test.rb +171 -26
- data/test/integration/minibundle_production_mode_test.rb +234 -42
- data/test/integration/ministamp_development_mode_test.rb +145 -0
- data/test/integration/{ministamp_test.rb → ministamp_production_mode_test.rb} +72 -23
- data/test/integration/static_files_as_asset_sources_test.rb +3 -0
- data/test/support/assertions.rb +24 -0
- data/test/support/fixture_config.rb +13 -10
- data/test/support/static_file_api_config.rb +9 -3
- data/test/support/test_case.rb +7 -6
- data/test/unit/asset_bundle_test.rb +16 -2
- data/test/unit/asset_file_registry_test.rb +117 -14
- data/test/unit/asset_tag_markup_test.rb +17 -5
- data/test/unit/bundle_file_properties_test.rb +32 -8
- data/test/unit/bundle_file_writing_test.rb +10 -11
- data/test/unit/development_file_collection_properties_test.rb +47 -15
- data/test/unit/environment_test.rb +0 -13
- data/test/unit/files_test.rb +46 -0
- data/test/unit/hashes_test.rb +41 -0
- data/test/unit/mini_bundle_block_test.rb +56 -1
- data/test/unit/mini_stamp_tag_test.rb +8 -8
- data/test/unit/stamp_file_properties_test.rb +32 -13
- data/test/unit/stamp_file_writing_test.rb +1 -5
- metadata +38 -16
- data/test/unit/development_file_collection_writing_test.rb +0 -43
- data/test/unit/jekyll_payload_test.rb +0 -64
@@ -1,44 +1,80 @@
|
|
1
|
-
require 'jekyll/minibundle/environment'
|
2
|
-
require 'jekyll/minibundle/development_file_collection'
|
3
1
|
require 'jekyll/minibundle/bundle_file'
|
2
|
+
require 'jekyll/minibundle/development_file'
|
3
|
+
require 'jekyll/minibundle/development_file_collection'
|
4
|
+
require 'jekyll/minibundle/environment'
|
4
5
|
require 'jekyll/minibundle/stamp_file'
|
5
6
|
|
6
7
|
module Jekyll::Minibundle
|
7
8
|
module AssetFileRegistry
|
8
9
|
class << self
|
9
10
|
def clear
|
10
|
-
@
|
11
|
+
@_files = {}
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
15
|
-
@_instances[asset_destination_path] ||= register_bundle_file(site, bundle_config)
|
14
|
+
def register_bundle_file(site, bundle_config)
|
15
|
+
register_file_for_bundle_block(BundleFile, site, bundle_config) { |file| [file] }
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
|
18
|
+
def register_development_file_collection(site, bundle_config)
|
19
|
+
register_file_for_bundle_block(DevelopmentFileCollection, site, bundle_config, &:files)
|
20
|
+
end
|
21
|
+
|
22
|
+
def register_stamp_file(site, asset_source_path, asset_destination_path)
|
23
|
+
register_file_for_stamp_tag(StampFile, site, asset_source_path, asset_destination_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def register_development_file(site, asset_source_path, asset_destination_path)
|
27
|
+
register_file_for_stamp_tag(DevelopmentFile, site, asset_source_path, asset_destination_path)
|
20
28
|
end
|
21
29
|
|
22
30
|
private
|
23
31
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
def register_file_for_bundle_block(file_class, site, bundle_config, &get_files)
|
33
|
+
asset_destination_path = "#{bundle_config.fetch('destination_path')}.#{bundle_config.fetch('type')}"
|
34
|
+
|
35
|
+
cached = @_files[asset_destination_path]
|
36
|
+
|
37
|
+
if cached
|
38
|
+
raise "minibundle block has same destination path as a ministamp tag: #{asset_destination_path}" if cached.fetch(:type) != :bundle
|
39
|
+
|
40
|
+
cached_file = cached.fetch(:file)
|
41
|
+
|
42
|
+
if bundle_config == cached.fetch(:config)
|
43
|
+
get_files.call(cached_file).each do |file|
|
44
|
+
site.static_files << file unless site.static_files.include?(file)
|
45
|
+
end
|
46
|
+
return cached_file
|
47
|
+
else
|
48
|
+
get_files.call(cached_file).each { |file| site.static_files.delete(file) }
|
49
|
+
end
|
29
50
|
end
|
30
|
-
end
|
31
51
|
|
32
|
-
|
33
|
-
|
52
|
+
new_file = file_class.new(site, bundle_config)
|
53
|
+
@_files[asset_destination_path] = {type: :bundle, file: new_file, config: bundle_config}
|
54
|
+
get_files.call(new_file).each { |file| site.static_files << file }
|
55
|
+
new_file
|
34
56
|
end
|
35
57
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
58
|
+
def register_file_for_stamp_tag(file_class, site, asset_source_path, asset_destination_path)
|
59
|
+
cached = @_files[asset_destination_path]
|
60
|
+
|
61
|
+
if cached
|
62
|
+
raise "ministamp tag has same destination path as a minibundle block: #{asset_destination_path}" if cached.fetch(:type) != :stamp
|
63
|
+
|
64
|
+
cached_file = cached.fetch(:file)
|
65
|
+
|
66
|
+
if asset_source_path == cached.fetch(:config)
|
67
|
+
site.static_files << cached_file unless site.static_files.include?(cached_file)
|
68
|
+
return cached_file
|
69
|
+
else
|
70
|
+
site.static_files.delete(cached_file)
|
71
|
+
end
|
41
72
|
end
|
73
|
+
|
74
|
+
new_file = file_class.new(site, asset_source_path, asset_destination_path)
|
75
|
+
@_files[asset_destination_path] = {type: :stamp, file: new_file, config: asset_source_path}
|
76
|
+
site.static_files << new_file
|
77
|
+
new_file
|
42
78
|
end
|
43
79
|
end
|
44
80
|
|
@@ -3,14 +3,15 @@ require 'cgi'
|
|
3
3
|
module Jekyll::Minibundle
|
4
4
|
module AssetTagMarkup
|
5
5
|
class << self
|
6
|
-
def make_markup(type, path, attributes)
|
6
|
+
def make_markup(type, baseurl, path, attributes)
|
7
|
+
url = make_url(baseurl, path)
|
7
8
|
case type
|
8
9
|
when :js
|
9
|
-
%{<script type="text/javascript" src="#{
|
10
|
+
%{<script type="text/javascript" src="#{url}"#{make_attributes(attributes)}></script>}
|
10
11
|
when :css
|
11
|
-
%{<link rel="stylesheet" href="#{
|
12
|
+
%{<link rel="stylesheet" href="#{url}"#{make_attributes(attributes)}>}
|
12
13
|
else
|
13
|
-
|
14
|
+
raise ArgumentError, "Unknown type for generating bundle markup: #{type}, #{path}"
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
@@ -21,6 +22,14 @@ module Jekyll::Minibundle
|
|
21
22
|
def make_attribute(name, value)
|
22
23
|
%{ #{name}="#{CGI.escape_html(value)}"}
|
23
24
|
end
|
25
|
+
|
26
|
+
def make_url(baseurl, path)
|
27
|
+
if baseurl.empty?
|
28
|
+
path
|
29
|
+
else
|
30
|
+
File.join(baseurl, path)
|
31
|
+
end
|
32
|
+
end
|
24
33
|
end
|
25
34
|
end
|
26
35
|
end
|
@@ -2,7 +2,6 @@ require 'jekyll/minibundle/asset_bundle'
|
|
2
2
|
require 'jekyll/minibundle/asset_file_operations'
|
3
3
|
require 'jekyll/minibundle/asset_file_properties'
|
4
4
|
require 'jekyll/minibundle/asset_stamp'
|
5
|
-
require 'jekyll/minibundle/asset_tag_markup'
|
6
5
|
|
7
6
|
module Jekyll::Minibundle
|
8
7
|
class BundleFile
|
@@ -17,7 +16,6 @@ module Jekyll::Minibundle
|
|
17
16
|
asset_source_dir = File.join(@site.source, config.fetch('source_dir'))
|
18
17
|
@asset_paths = config.fetch('assets').map { |asset_path| File.join(asset_source_dir, "#{asset_path}.#{@type}") }
|
19
18
|
@destination_path = config.fetch('destination_path')
|
20
|
-
@attributes = config.fetch('attributes')
|
21
19
|
@minifier_cmd = config.fetch('minifier_cmd')
|
22
20
|
@stamped_at = nil
|
23
21
|
@is_modified = false
|
@@ -25,7 +23,8 @@ module Jekyll::Minibundle
|
|
25
23
|
|
26
24
|
def destination_path_for_markup
|
27
25
|
# we must rebundle here, if at all, in order to make sure the
|
28
|
-
# markup and generated file have the same
|
26
|
+
# markup destination and generated file paths have the same
|
27
|
+
# fingerprint
|
29
28
|
if modified?
|
30
29
|
@stamped_at = mtime
|
31
30
|
@is_modified = true
|
@@ -33,7 +32,7 @@ module Jekyll::Minibundle
|
|
33
32
|
asset_bundle.make_bundle
|
34
33
|
end
|
35
34
|
|
36
|
-
|
35
|
+
asset_destination_path
|
37
36
|
end
|
38
37
|
|
39
38
|
def path
|
@@ -48,8 +47,12 @@ module Jekyll::Minibundle
|
|
48
47
|
"#{@destination_path}-#{asset_stamp}.#{@type}"
|
49
48
|
end
|
50
49
|
|
51
|
-
def
|
52
|
-
@
|
50
|
+
def extname
|
51
|
+
".#{@type}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def modified_time
|
55
|
+
@asset_paths.map { |f| File.stat(f).mtime }.max
|
53
56
|
end
|
54
57
|
|
55
58
|
# writes destination only after `destination_path_for_markup` has
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Jekyll::Minibundle
|
2
2
|
module Compatibility
|
3
|
+
LOG_TOPIC = 'Minibundle:'.freeze
|
4
|
+
|
3
5
|
class << self
|
4
6
|
# SafeYAML.load is introduced in Jekyll 2.0.0
|
5
7
|
if defined?(::SafeYAML) && ::SafeYAML.respond_to?(:load)
|
@@ -14,10 +16,18 @@ module Jekyll::Minibundle
|
|
14
16
|
|
15
17
|
# Jekyll.logger is introduced in Jekyll 1.0.3
|
16
18
|
if ::Jekyll.respond_to?(:logger)
|
19
|
+
def log_error(msg)
|
20
|
+
::Jekyll.logger.error(LOG_TOPIC, msg)
|
21
|
+
end
|
22
|
+
|
17
23
|
def log_info(msg)
|
18
|
-
::Jekyll.logger.info(
|
24
|
+
::Jekyll.logger.info(LOG_TOPIC, msg)
|
19
25
|
end
|
20
26
|
else
|
27
|
+
def log_error(msg)
|
28
|
+
$stderr.puts(msg)
|
29
|
+
end
|
30
|
+
|
21
31
|
def log_info(msg)
|
22
32
|
$stdout.puts(msg)
|
23
33
|
end
|
@@ -16,6 +16,12 @@ module Jekyll::Minibundle
|
|
16
16
|
@stamped_at = nil
|
17
17
|
end
|
18
18
|
|
19
|
+
alias destination_path_for_markup asset_destination_path
|
20
|
+
|
21
|
+
def extname
|
22
|
+
File.extname(asset_destination_path)
|
23
|
+
end
|
24
|
+
|
19
25
|
def write(site_destination_dir)
|
20
26
|
if modified?
|
21
27
|
@stamped_at = mtime
|
@@ -1,10 +1,12 @@
|
|
1
|
-
require 'jekyll/minibundle/asset_tag_markup'
|
2
1
|
require 'jekyll/minibundle/development_file'
|
3
2
|
|
4
3
|
module Jekyll::Minibundle
|
5
4
|
class DevelopmentFileCollection
|
5
|
+
attr_reader :files
|
6
|
+
|
6
7
|
def initialize(site, config)
|
7
8
|
@type = config.fetch('type')
|
9
|
+
|
8
10
|
asset_source_dir = File.join(site.source, config.fetch('source_dir'))
|
9
11
|
destination_path = config.fetch('destination_path')
|
10
12
|
|
@@ -14,21 +16,6 @@ module Jekyll::Minibundle
|
|
14
16
|
asset_destination = File.join(destination_path, asset_basename)
|
15
17
|
DevelopmentFile.new(site, asset_source, asset_destination)
|
16
18
|
end
|
17
|
-
|
18
|
-
@attributes = config.fetch('attributes')
|
19
|
-
end
|
20
|
-
|
21
|
-
def add_as_static_file_to(site)
|
22
|
-
# NOTE: We could optimize here by iterating over site's static
|
23
|
-
# files only once instead of per each of our file. Seems like a
|
24
|
-
# premature optimization for now, however.
|
25
|
-
@files.each { |f| f.add_as_static_file_to(site) }
|
26
|
-
end
|
27
|
-
|
28
|
-
def destination_path_for_markup
|
29
|
-
@files.
|
30
|
-
map { |f| AssetTagMarkup.make_markup(@type, f.asset_destination_path, @attributes) }.
|
31
|
-
join("\n")
|
32
19
|
end
|
33
20
|
end
|
34
21
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'jekyll/minibundle/hashes'
|
2
|
+
|
1
3
|
module Jekyll::Minibundle
|
2
4
|
module Environment
|
3
5
|
class << self
|
@@ -7,23 +9,14 @@ module Jekyll::Minibundle
|
|
7
9
|
end
|
8
10
|
|
9
11
|
def development?(site)
|
10
|
-
mode = ENV['JEKYLL_MINIBUNDLE_MODE'] || Environment.find_site_config(site,
|
12
|
+
mode = ENV['JEKYLL_MINIBUNDLE_MODE'] || Environment.find_site_config(site, %w{minibundle mode}, String)
|
11
13
|
mode == 'development'
|
12
14
|
end
|
13
15
|
|
14
16
|
def find_site_config(site, keys, type)
|
15
|
-
value =
|
17
|
+
value = Hashes.dig(site.config, *keys)
|
16
18
|
if value && !value.is_a?(type)
|
17
|
-
|
18
|
-
end
|
19
|
-
value
|
20
|
-
end
|
21
|
-
|
22
|
-
def traverse_keys(obj, keys)
|
23
|
-
value = obj
|
24
|
-
keys.each do |key|
|
25
|
-
return nil unless value
|
26
|
-
value = value[key]
|
19
|
+
raise "Invalid site configuration for key #{keys.join('.')}; expecting type #{type}"
|
27
20
|
end
|
28
21
|
value
|
29
22
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Jekyll::Minibundle
|
2
|
+
module Files
|
3
|
+
def self.read_last(path, max_size)
|
4
|
+
File.open(path, 'rb') do |file|
|
5
|
+
return '' if max_size < 1
|
6
|
+
|
7
|
+
file_size = file.stat.size
|
8
|
+
|
9
|
+
if file_size < max_size
|
10
|
+
file.read(file_size)
|
11
|
+
else
|
12
|
+
file.seek(file_size - max_size, ::IO::SEEK_SET)
|
13
|
+
file.read(max_size)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Jekyll::Minibundle
|
2
|
+
module Hashes
|
3
|
+
class << self
|
4
|
+
def dig(obj, *keys)
|
5
|
+
value = obj
|
6
|
+
keys.each do |key|
|
7
|
+
return nil unless value
|
8
|
+
value = value[key]
|
9
|
+
end
|
10
|
+
value
|
11
|
+
end
|
12
|
+
|
13
|
+
def pick(hash, *keys)
|
14
|
+
keys.each_with_object({}) do |key, acc|
|
15
|
+
acc[key] = hash.fetch(key)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,29 +1,33 @@
|
|
1
|
-
require 'jekyll/minibundle/
|
1
|
+
require 'jekyll/minibundle/hashes'
|
2
2
|
require 'jekyll/minibundle/compatibility'
|
3
3
|
require 'jekyll/minibundle/environment'
|
4
|
+
require 'jekyll/minibundle/asset_file_registry'
|
5
|
+
require 'jekyll/minibundle/asset_tag_markup'
|
4
6
|
|
5
7
|
module Jekyll::Minibundle
|
6
8
|
class MiniBundleBlock < Liquid::Block
|
7
9
|
def initialize(tag_name, type, _tokens)
|
8
10
|
super
|
9
11
|
@type = type.strip.downcase.to_sym
|
10
|
-
if @type.empty?
|
11
|
-
fail ArgumentError, "No asset type for minibundle block; pass value such as 'css' or 'js' as the argument"
|
12
|
-
end
|
12
|
+
raise ArgumentError, "No asset type for minibundle block; pass value such as 'css' or 'js' as the argument" if @type.empty?
|
13
13
|
end
|
14
14
|
|
15
15
|
def render(context)
|
16
16
|
site = context.registers.fetch(:site)
|
17
17
|
bundle_config = get_current_bundle_config(Compatibility.load_yaml(super), site)
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
baseurl = bundle_config.fetch('baseurl')
|
19
|
+
attributes = bundle_config.fetch('attributes')
|
20
|
+
|
21
|
+
register_asset_files(site, bundle_config).map do |file|
|
22
|
+
AssetTagMarkup.make_markup(@type, baseurl, file.destination_path_for_markup, attributes)
|
23
|
+
end.join("\n")
|
21
24
|
end
|
22
25
|
|
23
26
|
def self.default_bundle_config
|
24
27
|
{
|
25
28
|
'source_dir' => '_assets',
|
26
29
|
'destination_path' => 'assets/site',
|
30
|
+
'baseurl' => '',
|
27
31
|
'assets' => [],
|
28
32
|
'attributes' => {}
|
29
33
|
}
|
@@ -32,14 +36,50 @@ module Jekyll::Minibundle
|
|
32
36
|
private
|
33
37
|
|
34
38
|
def get_current_bundle_config(local_bundle_config, site)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
merge(
|
39
|
+
config =
|
40
|
+
MiniBundleBlock
|
41
|
+
.default_bundle_config
|
42
|
+
.merge(environment_bundle_config(site))
|
43
|
+
.merge(local_bundle_config)
|
44
|
+
.merge('type' => @type)
|
45
|
+
|
46
|
+
baseurl, destination_path = normalize_baseurl_and_destination_path(config.fetch('baseurl'), config.fetch('destination_path'))
|
47
|
+
|
48
|
+
config.merge('baseurl' => baseurl, 'destination_path' => destination_path)
|
39
49
|
end
|
40
50
|
|
41
51
|
def environment_bundle_config(site)
|
42
|
-
{
|
52
|
+
{'minifier_cmd' => Environment.minifier_command(site, @type)}
|
53
|
+
end
|
54
|
+
|
55
|
+
def normalize_baseurl_and_destination_path(baseurl, destination_path)
|
56
|
+
baseurl = '' if baseurl.nil?
|
57
|
+
|
58
|
+
unless destination_path.start_with?('/')
|
59
|
+
return [baseurl, destination_path]
|
60
|
+
end
|
61
|
+
|
62
|
+
normalized_baseurl = baseurl.empty? ? '/' : baseurl
|
63
|
+
normalized_destination_path = destination_path.sub(%r{\A/+}, '')
|
64
|
+
|
65
|
+
[normalized_baseurl, normalized_destination_path]
|
66
|
+
end
|
67
|
+
|
68
|
+
def register_asset_files(site, bundle_config)
|
69
|
+
registry_config = Hashes.pick(
|
70
|
+
bundle_config,
|
71
|
+
'type',
|
72
|
+
'source_dir',
|
73
|
+
'destination_path',
|
74
|
+
'assets',
|
75
|
+
'minifier_cmd'
|
76
|
+
)
|
77
|
+
|
78
|
+
if Environment.development?(site)
|
79
|
+
AssetFileRegistry.register_development_file_collection(site, registry_config).files
|
80
|
+
else
|
81
|
+
[AssetFileRegistry.register_bundle_file(site, registry_config)]
|
82
|
+
end
|
43
83
|
end
|
44
84
|
end
|
45
85
|
end
|
@@ -4,20 +4,39 @@ module Jekyll::Minibundle
|
|
4
4
|
class MiniStampTag < Liquid::Tag
|
5
5
|
def initialize(tag_name, text, _tokens)
|
6
6
|
super
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
|
8
|
+
@source_path, destination_path = text.split(/\s+/, 3)[0, 2]
|
9
|
+
|
10
|
+
if !@source_path || @source_path.empty?
|
11
|
+
raise ArgumentError, "No asset source for ministamp tag; pass value such as '_assets/site.css' as the first argument"
|
10
12
|
end
|
11
|
-
|
12
|
-
|
13
|
+
|
14
|
+
if !destination_path || destination_path.empty?
|
15
|
+
raise ArgumentError, "No asset destination for ministamp tag; pass value such as 'assets/site.css' as the second argument"
|
13
16
|
end
|
17
|
+
|
18
|
+
@baseurl, @destination_path = normalize_destination_path(destination_path)
|
14
19
|
end
|
15
20
|
|
16
21
|
def render(context)
|
17
22
|
site = context.registers.fetch(:site)
|
18
|
-
file =
|
19
|
-
|
20
|
-
|
23
|
+
file =
|
24
|
+
if Environment.development?(site)
|
25
|
+
AssetFileRegistry.register_development_file(site, @source_path, @destination_path)
|
26
|
+
else
|
27
|
+
AssetFileRegistry.register_stamp_file(site, @source_path, @destination_path)
|
28
|
+
end
|
29
|
+
@baseurl + file.destination_path_for_markup
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def normalize_destination_path(destination_path)
|
35
|
+
if destination_path.start_with?('/')
|
36
|
+
['/', destination_path.sub(%r{\A/+}, '')]
|
37
|
+
else
|
38
|
+
['', destination_path]
|
39
|
+
end
|
21
40
|
end
|
22
41
|
end
|
23
42
|
end
|