roadie 2.4.3 → 3.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +9 -14
- data/.yardopts +1 -1
- data/Changelog.md +22 -10
- data/Gemfile +3 -0
- data/Guardfile +11 -1
- data/README.md +165 -163
- data/Rakefile +2 -19
- data/lib/roadie.rb +14 -69
- data/lib/roadie/asset_provider.rb +7 -58
- data/lib/roadie/asset_scanner.rb +92 -0
- data/lib/roadie/document.rb +103 -0
- data/lib/roadie/errors.rb +57 -0
- data/lib/roadie/filesystem_provider.rb +21 -62
- data/lib/roadie/inliner.rb +71 -218
- data/lib/roadie/markup_improver.rb +88 -0
- data/lib/roadie/null_provider.rb +13 -0
- data/lib/roadie/null_url_rewriter.rb +12 -0
- data/lib/roadie/provider_list.rb +67 -0
- data/lib/roadie/rspec.rb +1 -0
- data/lib/roadie/rspec/asset_provider.rb +49 -0
- data/lib/roadie/selector.rb +42 -18
- data/lib/roadie/style_block.rb +33 -0
- data/lib/roadie/style_properties.rb +29 -0
- data/lib/roadie/style_property.rb +93 -0
- data/lib/roadie/stylesheet.rb +65 -0
- data/lib/roadie/url_generator.rb +126 -0
- data/lib/roadie/url_rewriter.rb +84 -0
- data/lib/roadie/version.rb +1 -1
- data/roadie.gemspec +6 -10
- data/spec/fixtures/big_em.css +1 -0
- data/spec/fixtures/stylesheets/green.css +1 -0
- data/spec/integration_spec.rb +125 -95
- data/spec/lib/roadie/asset_scanner_spec.rb +153 -0
- data/spec/lib/roadie/css_not_found_spec.rb +16 -0
- data/spec/lib/roadie/document_spec.rb +123 -0
- data/spec/lib/roadie/filesystem_provider_spec.rb +25 -72
- data/spec/lib/roadie/inliner_spec.rb +105 -537
- data/spec/lib/roadie/markup_improver_spec.rb +78 -0
- data/spec/lib/roadie/null_provider_spec.rb +21 -0
- data/spec/lib/roadie/null_url_rewriter_spec.rb +19 -0
- data/spec/lib/roadie/provider_list_spec.rb +81 -0
- data/spec/lib/roadie/selector_spec.rb +7 -5
- data/spec/lib/roadie/style_block_spec.rb +35 -0
- data/spec/lib/roadie/style_properties_spec.rb +61 -0
- data/spec/lib/roadie/style_property_spec.rb +82 -0
- data/spec/lib/roadie/stylesheet_spec.rb +41 -0
- data/spec/lib/roadie/test_provider_spec.rb +29 -0
- data/spec/lib/roadie/url_generator_spec.rb +120 -0
- data/spec/lib/roadie/url_rewriter_spec.rb +79 -0
- data/spec/shared_examples/asset_provider.rb +11 -0
- data/spec/shared_examples/url_rewriter.rb +23 -0
- data/spec/spec_helper.rb +5 -60
- data/spec/support/have_node_matcher.rb +2 -2
- data/spec/support/have_selector_matcher.rb +1 -1
- data/spec/support/have_styling_matcher.rb +48 -14
- data/spec/support/test_provider.rb +13 -0
- metadata +73 -177
- data/Appraisals +0 -15
- data/gemfiles/rails_3.0.gemfile +0 -7
- data/gemfiles/rails_3.0.gemfile.lock +0 -123
- data/gemfiles/rails_3.1.gemfile +0 -7
- data/gemfiles/rails_3.1.gemfile.lock +0 -126
- data/gemfiles/rails_3.2.gemfile +0 -7
- data/gemfiles/rails_3.2.gemfile.lock +0 -124
- data/gemfiles/rails_4.0.gemfile +0 -7
- data/gemfiles/rails_4.0.gemfile.lock +0 -119
- data/lib/roadie/action_mailer_extensions.rb +0 -95
- data/lib/roadie/asset_pipeline_provider.rb +0 -28
- data/lib/roadie/css_file_not_found.rb +0 -22
- data/lib/roadie/railtie.rb +0 -39
- data/lib/roadie/style_declaration.rb +0 -42
- data/spec/fixtures/app/assets/stylesheets/integration.css +0 -10
- data/spec/fixtures/public/stylesheets/integration.css +0 -10
- data/spec/fixtures/views/integration_mailer/marketing.html.erb +0 -2
- data/spec/fixtures/views/integration_mailer/notification.html.erb +0 -8
- data/spec/fixtures/views/integration_mailer/notification.text.erb +0 -6
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +0 -227
- data/spec/lib/roadie/asset_pipeline_provider_spec.rb +0 -65
- data/spec/lib/roadie/css_file_not_found_spec.rb +0 -29
- data/spec/lib/roadie/style_declaration_spec.rb +0 -49
- data/spec/lib/roadie_spec.rb +0 -101
- data/spec/shared_examples/asset_provider_examples.rb +0 -11
- data/spec/support/anonymous_mailer.rb +0 -21
- data/spec/support/change_url_options.rb +0 -5
- data/spec/support/parse_styling.rb +0 -25
@@ -0,0 +1,13 @@
|
|
1
|
+
module Roadie
|
2
|
+
# An asset provider that returns empty stylesheets for any name.
|
3
|
+
#
|
4
|
+
# Use it to ignore missing assets or in your tests when you need a provider
|
5
|
+
# but you do not care what it contains or that it is even referenced at all.
|
6
|
+
class NullProvider
|
7
|
+
def find_stylesheet(name) empty_stylesheet end
|
8
|
+
def find_stylesheet!(name) empty_stylesheet end
|
9
|
+
|
10
|
+
private
|
11
|
+
def empty_stylesheet() Stylesheet.new "(null)", "" end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Roadie
|
2
|
+
# @api private
|
3
|
+
# Null Object for the URL rewriter role.
|
4
|
+
#
|
5
|
+
# Used whenever client does not pass any URL options and no URL rewriting
|
6
|
+
# should take place.
|
7
|
+
class NullUrlRewriter
|
8
|
+
def initialize(generator = nil) end
|
9
|
+
def transform_dom(dom) end
|
10
|
+
def transform_css(css) end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Roadie
|
4
|
+
# An asset provider that just composes a list of other asset providers.
|
5
|
+
#
|
6
|
+
# Give it a list of providers and they will all be tried in order.
|
7
|
+
#
|
8
|
+
# {ProviderList} behaves like an Array, *and* an asset provider, and can be coerced into an array.
|
9
|
+
class ProviderList
|
10
|
+
extend Forwardable
|
11
|
+
include Enumerable
|
12
|
+
include AssetProvider
|
13
|
+
|
14
|
+
# Wrap a single provider, or a list of providers into a {ProviderList}.
|
15
|
+
#
|
16
|
+
# @overload wrap(provider_list)
|
17
|
+
# @param [ProviderList] provider_list An actual instance of {ProviderList}.
|
18
|
+
# @return The passed in provider_list
|
19
|
+
#
|
20
|
+
# @overload wrap(provider)
|
21
|
+
# @param [asset provider] provider
|
22
|
+
# @return a new {ProviderList} with just the passed provider in it
|
23
|
+
#
|
24
|
+
# @overload wrap(provider1, provider2, ...)
|
25
|
+
# @return a new {ProviderList} with all the passed providers in it.
|
26
|
+
def self.wrap(*providers)
|
27
|
+
if providers.size == 1 && providers.first.class == self
|
28
|
+
providers.first
|
29
|
+
else
|
30
|
+
new(providers.flatten)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(providers)
|
35
|
+
@providers = providers
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Stylesheet, nil]
|
39
|
+
def find_stylesheet(name)
|
40
|
+
@providers.each do |provider|
|
41
|
+
css = provider.find_stylesheet(name)
|
42
|
+
return css if css
|
43
|
+
end
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# ProviderList can be coerced to an array. This makes Array#flatten work
|
48
|
+
# with it, among other things.
|
49
|
+
def to_ary() to_a end
|
50
|
+
|
51
|
+
# @!method each
|
52
|
+
# @see Array#each
|
53
|
+
# @!method size
|
54
|
+
# @see Array#size
|
55
|
+
# @!method push
|
56
|
+
# @see Array#push
|
57
|
+
# @!method <<
|
58
|
+
# @see Array#<<
|
59
|
+
# @!method pop
|
60
|
+
# @see Array#pop
|
61
|
+
# @!method unshift
|
62
|
+
# @see Array#unshift
|
63
|
+
# @!method shift
|
64
|
+
# @see Array#shift
|
65
|
+
def_delegators :@providers, :each, :size, :push, :<<, :pop, :unshift, :shift
|
66
|
+
end
|
67
|
+
end
|
data/lib/roadie/rspec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'roadie/rspec/asset_provider'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
shared_examples_for "roadie asset provider" do |options|
|
2
|
+
valid_name = options[:valid_name] or raise "You must provide a :valid_name option to the shared examples"
|
3
|
+
invalid_name = options[:invalid_name] or raise "You must provide an :invalid_name option to the shared examples"
|
4
|
+
|
5
|
+
def verify_stylesheet(stylesheet)
|
6
|
+
stylesheet.should_not be_nil
|
7
|
+
|
8
|
+
# Name
|
9
|
+
stylesheet.name.should be_a(String)
|
10
|
+
stylesheet.name.should_not be_empty
|
11
|
+
|
12
|
+
# We do not want to force clients to always return non-empty files.
|
13
|
+
# Stylesheet#initialize should crash when given a non-valid CSS (like nil,
|
14
|
+
# for example)
|
15
|
+
# stylesheet.blocks.should_not be_empty
|
16
|
+
end
|
17
|
+
|
18
|
+
it "responds to #find_stylesheet" do
|
19
|
+
subject.should respond_to(:find_stylesheet)
|
20
|
+
subject.method(:find_stylesheet).arity.should == 1
|
21
|
+
end
|
22
|
+
|
23
|
+
it "responds to #find_stylesheet!" do
|
24
|
+
subject.should respond_to(:find_stylesheet!)
|
25
|
+
subject.method(:find_stylesheet!).arity.should == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#find_stylesheet" do
|
29
|
+
it "can find a stylesheet" do
|
30
|
+
verify_stylesheet subject.find_stylesheet(valid_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "cannot find an invalid stylesheet" do
|
34
|
+
subject.find_stylesheet(invalid_name).should be_nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#find_stylesheet!" do
|
39
|
+
it "can find a stylesheet" do
|
40
|
+
verify_stylesheet subject.find_stylesheet!(valid_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "raises Roadie::CssNotFound on invalid stylesheets" do
|
44
|
+
expect {
|
45
|
+
subject.find_stylesheet!(invalid_name)
|
46
|
+
}.to raise_error Roadie::CssNotFound, Regexp.new(Regexp.quote(invalid_name))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/roadie/selector.rb
CHANGED
@@ -1,24 +1,46 @@
|
|
1
1
|
module Roadie
|
2
|
+
# @api private
|
3
|
+
#
|
4
|
+
# A selector is a domain object for a CSS selector, such as:
|
5
|
+
# body
|
6
|
+
# a:hover
|
7
|
+
# input::placeholder
|
8
|
+
# p:nth-of-child(4n+1) .important a img
|
9
|
+
#
|
10
|
+
# "Selectors" such as "strong, em" are actually two selectors and should be
|
11
|
+
# represented as two instances of this class.
|
12
|
+
#
|
13
|
+
# This class can also calculate specificity for the selector and answer a few
|
14
|
+
# questions about them.
|
15
|
+
#
|
16
|
+
# Selectors can be coerced into Strings, so they should be transparent to use
|
17
|
+
# anywhere a String is expected.
|
2
18
|
class Selector
|
3
|
-
def initialize(selector)
|
19
|
+
def initialize(selector, specificity = nil)
|
4
20
|
@selector = selector.to_s.strip
|
21
|
+
@specificity = specificity
|
5
22
|
end
|
6
23
|
|
24
|
+
# Returns the specificity of the selector, calculating it if needed.
|
7
25
|
def specificity
|
8
26
|
@specificity ||= CssParser.calculate_specificity selector
|
9
27
|
end
|
10
28
|
|
29
|
+
# Returns whenever or not a selector can be inlined.
|
30
|
+
# It's impossible to inline properties that applies to a pseudo element
|
31
|
+
# (like +::placeholder+, +::before+) or a pseudo function (like +:active+).
|
32
|
+
#
|
33
|
+
# We cannot inline styles that appear inside "@" constructs, like +@keyframes+.
|
11
34
|
def inlinable?
|
12
35
|
!(pseudo_element? || at_rule? || pseudo_function?)
|
13
36
|
end
|
14
37
|
|
15
|
-
def to_s
|
16
|
-
selector
|
17
|
-
end
|
18
|
-
|
38
|
+
def to_s() selector end
|
19
39
|
def to_str() to_s end
|
20
40
|
def inspect() selector.inspect end
|
21
41
|
|
42
|
+
# {Selector}s are equal to other {Selector}s if, and only if, their string
|
43
|
+
# representations are equal.
|
22
44
|
def ==(other)
|
23
45
|
if other.is_a?(self.class)
|
24
46
|
other.selector == selector
|
@@ -28,23 +50,25 @@ module Roadie
|
|
28
50
|
end
|
29
51
|
|
30
52
|
protected
|
31
|
-
|
53
|
+
attr_reader :selector
|
32
54
|
|
33
55
|
private
|
34
|
-
|
35
|
-
|
36
|
-
|
56
|
+
BAD_PSEUDO_FUNCTIONS = %w[
|
57
|
+
:active :focus :hover :link :target :visited
|
58
|
+
:-ms-input-placeholder :-moz-placeholder
|
59
|
+
:before :after
|
60
|
+
].freeze
|
37
61
|
|
38
|
-
|
39
|
-
|
40
|
-
|
62
|
+
def pseudo_element?
|
63
|
+
selector.include? '::'
|
64
|
+
end
|
41
65
|
|
42
|
-
|
43
|
-
|
44
|
-
|
66
|
+
def at_rule?
|
67
|
+
selector[0, 1] == '@'
|
68
|
+
end
|
45
69
|
|
46
|
-
|
47
|
-
|
48
|
-
|
70
|
+
def pseudo_function?
|
71
|
+
BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) }
|
72
|
+
end
|
49
73
|
end
|
50
74
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Roadie
|
4
|
+
# @api private
|
5
|
+
# A style block is the combination of a {Selector} and a list of {StyleProperty}.
|
6
|
+
class StyleBlock
|
7
|
+
extend Forwardable
|
8
|
+
attr_reader :selector, :properties
|
9
|
+
|
10
|
+
# @param [Selector] selector
|
11
|
+
# @param [Array<StyleProperty>] properties
|
12
|
+
def initialize(selector, properties)
|
13
|
+
@selector = selector
|
14
|
+
# TODO: Should we use {StyleProperties} instead? Why? Why not?
|
15
|
+
@properties = properties
|
16
|
+
end
|
17
|
+
|
18
|
+
# @!method specificity
|
19
|
+
# @see Selector#specificity
|
20
|
+
# @!method inlinable?
|
21
|
+
# @see Selector#inlinable?
|
22
|
+
def_delegators :selector, :specificity, :inlinable?
|
23
|
+
# @!method selector_string
|
24
|
+
# @see Selector#to_s
|
25
|
+
def_delegator :selector, :to_s, :selector_string
|
26
|
+
|
27
|
+
# String representation of the style block. This is valid CSS and can be
|
28
|
+
# used in the DOM.
|
29
|
+
def to_s
|
30
|
+
"#{selector}{#{properties.map(&:to_s).join(';')}}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Roadie
|
2
|
+
# @api private
|
3
|
+
# Stores orphan style properties as they are being merged into specific
|
4
|
+
# element's "style" attribute.
|
5
|
+
class StyleProperties
|
6
|
+
attr_reader :properties
|
7
|
+
|
8
|
+
def initialize(properties)
|
9
|
+
@properties = properties
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(new_properties)
|
13
|
+
StyleProperties.new(properties + properties_of(new_properties))
|
14
|
+
end
|
15
|
+
|
16
|
+
def merge!(new_properties)
|
17
|
+
@properties += properties_of(new_properties)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@properties.sort.map(&:to_s).join(";")
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def properties_of(object)
|
26
|
+
object.respond_to?(:properties) ? object.properties : object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Roadie
|
2
|
+
# @api private
|
3
|
+
# Domain object for a CSS property such as "color: red !important".
|
4
|
+
#
|
5
|
+
# @attr_reader [String] property name of the property (such as "font-size").
|
6
|
+
# @attr_reader [String] value value of the property (such as "5px solid green").
|
7
|
+
# @attr_reader [Boolean] important if the property is "!important".
|
8
|
+
# @attr_reader [Integer] specificity specificity of parent {Selector}. Used to compare/sort.
|
9
|
+
class StyleProperty
|
10
|
+
include Comparable
|
11
|
+
|
12
|
+
attr_reader :value, :important, :specificity
|
13
|
+
|
14
|
+
# @todo Rename #property to #name
|
15
|
+
attr_reader :property
|
16
|
+
|
17
|
+
# Parse a property string.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# property = Roadie::StyleProperty.parse("color: green")
|
21
|
+
# property.property # => "color"
|
22
|
+
# property.value # => "green"
|
23
|
+
# property.important? # => false
|
24
|
+
def self.parse(declaration, specificity)
|
25
|
+
allocate.send :read_declaration!, declaration, specificity
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(property, value, important, specificity)
|
29
|
+
@property = property
|
30
|
+
@value = value
|
31
|
+
@important = important
|
32
|
+
@specificity = specificity
|
33
|
+
end
|
34
|
+
|
35
|
+
def important?
|
36
|
+
@important
|
37
|
+
end
|
38
|
+
|
39
|
+
# Compare another {StyleProperty}. Important styles are "greater than"
|
40
|
+
# non-important ones; otherwise the specificity declares order.
|
41
|
+
def <=>(other)
|
42
|
+
if important == other.important
|
43
|
+
specificity <=> other.specificity
|
44
|
+
else
|
45
|
+
important ? 1 : -1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
[property, value_with_important].join(':')
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#{to_s} (#{specificity})"
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
def read_declaration!(declaration, specificity)
|
59
|
+
if (matches = DECLARATION_MATCHER.match(declaration))
|
60
|
+
initialize matches[:property], matches[:value].strip, !!matches[:important], specificity
|
61
|
+
self
|
62
|
+
else
|
63
|
+
raise UnparseableDeclaration, "Cannot parse declaration #{declaration.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
DECLARATION_MATCHER = %r{
|
69
|
+
\A\s*
|
70
|
+
(?:
|
71
|
+
# !important declaration
|
72
|
+
(?<property>[^:]+):\s?
|
73
|
+
(?<value>.*?)
|
74
|
+
(?<important>\s!important)
|
75
|
+
;?
|
76
|
+
|
|
77
|
+
# normal declaration
|
78
|
+
(?<property>[^:]+):\s?
|
79
|
+
(?<value>[^;]+)
|
80
|
+
;?
|
81
|
+
)
|
82
|
+
\s*\Z
|
83
|
+
}x.freeze
|
84
|
+
|
85
|
+
def value_with_important
|
86
|
+
if important
|
87
|
+
"#{value} !important"
|
88
|
+
else
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Roadie
|
2
|
+
# Domain object that represents a stylesheet (from disc, perhaps).
|
3
|
+
#
|
4
|
+
# It has a name and a list of {StyleBlock}s.
|
5
|
+
#
|
6
|
+
# @attr_reader [String] name the name of the stylesheet ("stylesheets/main.css", "Admin user styles", etc.). The name of the stylesheet will be visible if any errors occur.
|
7
|
+
# @attr_reader [Array<StyleBlock>] blocks
|
8
|
+
class Stylesheet
|
9
|
+
attr_reader :name, :blocks
|
10
|
+
|
11
|
+
# Parses the CSS string into a {StyleBlock}s and stores it.
|
12
|
+
#
|
13
|
+
# @param [String] name
|
14
|
+
# @param [String] css
|
15
|
+
def initialize(name, css)
|
16
|
+
@name = name
|
17
|
+
@blocks = parse_blocks(css)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @yield [selector, properties]
|
21
|
+
# @yieldparam [Selector] selector
|
22
|
+
# @yieldparam [Array<StyleProperty>] properties
|
23
|
+
def each_inlinable_block(&block)
|
24
|
+
# #map and then #each in order to support chained enumerations, etc. if
|
25
|
+
# no block is provided
|
26
|
+
inlinable_blocks.map { |style_block|
|
27
|
+
[style_block.selector, style_block.properties]
|
28
|
+
}.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
blocks.join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def inlinable_blocks
|
37
|
+
blocks.select(&:inlinable?)
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_blocks(css)
|
41
|
+
blocks = []
|
42
|
+
setup_parser(css).each_selector do |selector_string, declarations, specificity|
|
43
|
+
blocks << create_style_block(selector_string, declarations, specificity)
|
44
|
+
end
|
45
|
+
blocks
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_style_block(selector_string, declarations, specificity)
|
49
|
+
StyleBlock.new(
|
50
|
+
Selector.new(selector_string, specificity),
|
51
|
+
parse_declarations(declarations, specificity)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def setup_parser(css)
|
56
|
+
parser = CssParser::Parser.new
|
57
|
+
parser.add_block! css
|
58
|
+
parser
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_declarations(declarations, specificity)
|
62
|
+
declarations.split(';').map { |declaration| StyleProperty.parse(declaration, specificity) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|