feedjira 2.2.0 → 3.0.0.beta1
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 +5 -5
- data/.rubocop.yml +635 -6
- data/.travis.yml +1 -1
- data/CHANGELOG.md +6 -12
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -5
- data/README.md +37 -99
- data/Rakefile +5 -5
- data/feedjira.gemspec +27 -19
- data/lib/feedjira.rb +69 -41
- data/lib/feedjira/configuration.rb +3 -8
- data/lib/feedjira/core_ext.rb +3 -3
- data/lib/feedjira/core_ext/date.rb +1 -1
- data/lib/feedjira/core_ext/time.rb +2 -2
- data/lib/feedjira/date_time_utilities.rb +2 -2
- data/lib/feedjira/date_time_utilities/date_time_pattern_parser.rb +2 -2
- data/lib/feedjira/feed.rb +10 -80
- data/lib/feedjira/feed_entry_utilities.rb +4 -4
- data/lib/feedjira/parser.rb +4 -1
- data/lib/feedjira/parser/atom.rb +3 -3
- data/lib/feedjira/parser/atom_entry.rb +1 -1
- data/lib/feedjira/parser/atom_feed_burner.rb +4 -4
- data/lib/feedjira/parser/atom_feed_burner_entry.rb +1 -1
- data/lib/feedjira/parser/atom_youtube.rb +2 -2
- data/lib/feedjira/parser/atom_youtube_entry.rb +1 -1
- data/lib/feedjira/parser/google_docs_atom.rb +3 -3
- data/lib/feedjira/parser/google_docs_atom_entry.rb +1 -1
- data/lib/feedjira/parser/itunes_rss_item.rb +1 -1
- data/lib/feedjira/parser/json_feed.rb +39 -0
- data/lib/feedjira/parser/json_feed_item.rb +51 -0
- data/lib/feedjira/parser/podlove_chapter.rb +1 -1
- data/lib/feedjira/parser/rss.rb +1 -1
- data/lib/feedjira/parser/rss_entry.rb +5 -1
- data/lib/feedjira/parser/rss_feed_burner.rb +1 -1
- data/lib/feedjira/preprocessor.rb +1 -1
- data/lib/feedjira/version.rb +1 -1
- data/spec/feedjira/configuration_spec.rb +9 -16
- data/spec/feedjira/date_time_utilities_spec.rb +20 -20
- data/spec/feedjira/feed_entry_utilities_spec.rb +18 -18
- data/spec/feedjira/feed_spec.rb +15 -229
- data/spec/feedjira/feed_utilities_spec.rb +72 -72
- data/spec/feedjira/parser/atom_entry_spec.rb +34 -34
- data/spec/feedjira/parser/atom_feed_burner_entry_spec.rb +16 -16
- data/spec/feedjira/parser/atom_feed_burner_spec.rb +121 -119
- data/spec/feedjira/parser/atom_spec.rb +78 -76
- data/spec/feedjira/parser/atom_youtube_entry_spec.rb +38 -38
- data/spec/feedjira/parser/atom_youtube_spec.rb +15 -15
- data/spec/feedjira/parser/google_docs_atom_entry_spec.rb +8 -8
- data/spec/feedjira/parser/google_docs_atom_spec.rb +23 -21
- data/spec/feedjira/parser/itunes_rss_item_spec.rb +37 -37
- data/spec/feedjira/parser/itunes_rss_owner_spec.rb +5 -5
- data/spec/feedjira/parser/itunes_rss_spec.rb +118 -116
- data/spec/feedjira/parser/json_feed_item_spec.rb +79 -0
- data/spec/feedjira/parser/json_feed_spec.rb +53 -0
- data/spec/feedjira/parser/podlove_chapter_spec.rb +12 -12
- data/spec/feedjira/parser/rss_entry_spec.rb +30 -30
- data/spec/feedjira/parser/rss_feed_burner_entry_spec.rb +32 -32
- data/spec/feedjira/parser/rss_feed_burner_spec.rb +47 -45
- data/spec/feedjira/parser/rss_spec.rb +36 -36
- data/spec/feedjira/preprocessor_spec.rb +6 -6
- data/spec/feedjira_spec.rb +145 -0
- data/spec/sample_feeds.rb +27 -26
- data/spec/sample_feeds/HuffPostCanada.xml +279 -0
- data/spec/sample_feeds/json_feed.json +156 -0
- data/spec/spec_helper.rb +5 -5
- metadata +31 -49
- data/fixtures/vcr_cassettes/fetch_failure.yml +0 -62
- data/fixtures/vcr_cassettes/parse_error.yml +0 -222
- data/fixtures/vcr_cassettes/success.yml +0 -281
- data/spec/sample_feeds/InvalidDateFormat.xml +0 -20
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe Feedjira::Parser::AtomFeedBurnerEntry do
|
4
4
|
before(:each) do
|
@@ -10,45 +10,45 @@ describe Feedjira::Parser::AtomFeedBurnerEntry do
|
|
10
10
|
@entry = feed.entries.first
|
11
11
|
end
|
12
12
|
|
13
|
-
it
|
14
|
-
expect(@entry.title).to eq
|
13
|
+
it "should parse the title" do
|
14
|
+
expect(@entry.title).to eq "Making a Ruby C library even faster"
|
15
15
|
end
|
16
16
|
|
17
17
|
it "should be able to fetch a url via the 'alternate' rel if no origLink exists" do # rubocop:disable Metrics/LineLength
|
18
18
|
xml = File.read("#{File.dirname(__FILE__)}/../../sample_feeds/PaulDixExplainsNothingAlternate.xml") # rubocop:disable Metrics/LineLength
|
19
19
|
entry = Feedjira::Parser::AtomFeedBurner.parse(xml).entries.first
|
20
|
-
expect(entry.url).to eq
|
20
|
+
expect(entry.url).to eq("http://feeds.feedburner.com/~r/PaulDixExplainsNothing/~3/519925023/making-a-ruby-c-library-even-faster.html") # rubocop:disable Metrics/LineLength
|
21
21
|
end
|
22
22
|
|
23
|
-
it
|
24
|
-
expect(@entry.url).to eq
|
23
|
+
it "should parse the url" do
|
24
|
+
expect(@entry.url).to eq "http://www.pauldix.net/2009/01/making-a-ruby-c-library-even-faster.html"
|
25
25
|
end
|
26
26
|
|
27
|
-
it
|
27
|
+
it "should parse the url when there is no alternate" do
|
28
28
|
xml = File.read("#{File.dirname(__FILE__)}/../../sample_feeds/FeedBurnerUrlNoAlternate.xml") # rubocop:disable Metrics/LineLength
|
29
29
|
entry = Feedjira::Parser::AtomFeedBurner.parse(xml).entries.first
|
30
|
-
expect(entry.url).to eq
|
30
|
+
expect(entry.url).to eq "http://example.com/QQQQ.html"
|
31
31
|
end
|
32
32
|
|
33
|
-
it
|
34
|
-
expect(@entry.author).to eq
|
33
|
+
it "should parse the author" do
|
34
|
+
expect(@entry.author).to eq "Paul Dix"
|
35
35
|
end
|
36
36
|
|
37
|
-
it
|
37
|
+
it "should parse the content" do
|
38
38
|
expect(@entry.content).to eq sample_feedburner_atom_entry_content
|
39
39
|
end
|
40
40
|
|
41
|
-
it
|
41
|
+
it "should provide a summary" do
|
42
42
|
summary = "Last week I released the first version of a SAX based XML parsing library called SAX-Machine. It uses Nokogiri, which uses libxml, so it's pretty fast. However, I felt that it could be even faster. The only question was how..." # rubocop:disable Metrics/LineLength
|
43
43
|
expect(@entry.summary).to eq summary
|
44
44
|
end
|
45
45
|
|
46
|
-
it
|
47
|
-
published = Time.parse_safely
|
46
|
+
it "should parse the published date" do
|
47
|
+
published = Time.parse_safely "Thu Jan 22 15:50:22 UTC 2009"
|
48
48
|
expect(@entry.published).to eq published
|
49
49
|
end
|
50
50
|
|
51
|
-
it
|
52
|
-
expect(@entry.categories).to eq [
|
51
|
+
it "should parse the categories" do
|
52
|
+
expect(@entry.categories).to eq ["Ruby", "Another Category"]
|
53
53
|
end
|
54
54
|
end
|
@@ -1,124 +1,126 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
module Feedjira
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Feedjira
|
4
|
+
module Parser
|
5
|
+
describe "#will_parse?" do
|
6
|
+
it "should return true for a feedburner atom feed" do
|
7
|
+
expect(AtomFeedBurner).to be_able_to_parse(sample_feedburner_atom_feed)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return false for an rdf feed" do
|
11
|
+
expect(AtomFeedBurner).to_not be_able_to_parse(sample_rdf_feed)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should return false for a regular atom feed" do
|
15
|
+
expect(AtomFeedBurner).to_not be_able_to_parse(sample_atom_feed)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return false for an rss feedburner feed" do
|
19
|
+
expect(AtomFeedBurner).to_not be_able_to_parse sample_rss_feed_burner_feed
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "parsing old style feeds" do
|
24
|
+
before(:each) do
|
25
|
+
@feed = AtomFeedBurner.parse(sample_feedburner_atom_feed)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should parse the title" do
|
29
|
+
expect(@feed.title).to eq "Paul Dix Explains Nothing"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should parse the description" do
|
33
|
+
description = "Entrepreneurship, programming, software development, politics, NYC, and random thoughts." # rubocop:disable Metrics/LineLength
|
34
|
+
expect(@feed.description).to eq description
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should parse the url" do
|
38
|
+
expect(@feed.url).to eq "http://www.pauldix.net/"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should parse the feed_url" do
|
42
|
+
expect(@feed.feed_url).to eq "http://feeds.feedburner.com/PaulDixExplainsNothing"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should parse no hub urls" do
|
46
|
+
expect(@feed.hubs.count).to eq 0
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should parse hub urls" do
|
50
|
+
AtomFeedBurner.preprocess_xml = false
|
51
|
+
feed_with_hub = AtomFeedBurner.parse(load_sample("TypePadNews.xml"))
|
52
|
+
expect(feed_with_hub.hubs.count).to eq 1
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should parse entries" do
|
56
|
+
expect(@feed.entries.size).to eq 5
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should change url" do
|
60
|
+
new_url = "http://some.url.com"
|
61
|
+
expect { @feed.url = new_url }.not_to raise_error
|
62
|
+
expect(@feed.url).to eq new_url
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should change feed_url" do
|
66
|
+
new_url = "http://some.url.com"
|
67
|
+
expect { @feed.feed_url = new_url }.not_to raise_error
|
68
|
+
expect(@feed.feed_url).to eq new_url
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "parsing alternate style feeds" do
|
73
|
+
before(:each) do
|
74
|
+
@feed = AtomFeedBurner.parse(sample_feedburner_atom_feed_alternate)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should parse the title" do
|
78
|
+
expect(@feed.title).to eq "Giant Robots Smashing Into Other Giant Robots"
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should parse the description" do
|
82
|
+
description = "Written by thoughtbot"
|
83
|
+
expect(@feed.description).to eq description
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should parse the url" do
|
87
|
+
expect(@feed.url).to eq "https://robots.thoughtbot.com"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should parse the feed_url" do
|
91
|
+
expect(@feed.feed_url).to eq "http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should parse hub urls" do
|
95
|
+
expect(@feed.hubs.count).to eq 1
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should parse entries" do
|
99
|
+
expect(@feed.entries.size).to eq 3
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should change url" do
|
103
|
+
new_url = "http://some.url.com"
|
104
|
+
expect { @feed.url = new_url }.not_to raise_error
|
105
|
+
expect(@feed.url).to eq new_url
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should change feed_url" do
|
109
|
+
new_url = "http://some.url.com"
|
110
|
+
expect { @feed.feed_url = new_url }.not_to raise_error
|
111
|
+
expect(@feed.feed_url).to eq new_url
|
112
|
+
end
|
68
113
|
end
|
69
|
-
end
|
70
|
-
|
71
|
-
describe 'parsing alternate style feeds' do
|
72
|
-
before(:each) do
|
73
|
-
@feed = AtomFeedBurner.parse(sample_feedburner_atom_feed_alternate)
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'should parse the title' do
|
77
|
-
expect(@feed.title).to eq 'Giant Robots Smashing Into Other Giant Robots'
|
78
|
-
end
|
79
|
-
|
80
|
-
it 'should parse the description' do
|
81
|
-
description = 'Written by thoughtbot'
|
82
|
-
expect(@feed.description).to eq description
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'should parse the url' do
|
86
|
-
expect(@feed.url).to eq 'https://robots.thoughtbot.com'
|
87
|
-
end
|
88
|
-
|
89
|
-
it 'should parse the feed_url' do
|
90
|
-
expect(@feed.feed_url).to eq 'http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots'
|
91
|
-
end
|
92
|
-
|
93
|
-
it 'should parse hub urls' do
|
94
|
-
expect(@feed.hubs.count).to eq 1
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'should parse entries' do
|
98
|
-
expect(@feed.entries.size).to eq 3
|
99
|
-
end
|
100
|
-
|
101
|
-
it 'should change url' do
|
102
|
-
new_url = 'http://some.url.com'
|
103
|
-
expect { @feed.url = new_url }.not_to raise_error
|
104
|
-
expect(@feed.url).to eq new_url
|
105
|
-
end
|
106
|
-
|
107
|
-
it 'should change feed_url' do
|
108
|
-
new_url = 'http://some.url.com'
|
109
|
-
expect { @feed.feed_url = new_url }.not_to raise_error
|
110
|
-
expect(@feed.feed_url).to eq new_url
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
describe 'preprocessing' do
|
115
|
-
it 'retains markup in xhtml content' do
|
116
|
-
AtomFeedBurner.preprocess_xml = true
|
117
|
-
|
118
|
-
feed = AtomFeedBurner.parse sample_feed_burner_atom_xhtml_feed
|
119
|
-
entry = feed.entries.first
|
120
114
|
|
121
|
-
|
115
|
+
describe "preprocessing" do
|
116
|
+
it "retains markup in xhtml content" do
|
117
|
+
AtomFeedBurner.preprocess_xml = true
|
118
|
+
|
119
|
+
feed = AtomFeedBurner.parse sample_feed_burner_atom_xhtml_feed
|
120
|
+
entry = feed.entries.first
|
121
|
+
|
122
|
+
expect(entry.content).to match(/\A\<p/)
|
123
|
+
end
|
122
124
|
end
|
123
125
|
end
|
124
126
|
end
|
@@ -1,104 +1,106 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
|
-
module Feedjira
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Feedjira
|
4
|
+
module Parser
|
5
|
+
describe "#will_parse?" do
|
6
|
+
it "should return true for an atom feed" do
|
7
|
+
expect(Atom).to be_able_to_parse(sample_atom_feed)
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
it "should return false for an rdf feed" do
|
11
|
+
expect(Atom).to_not be_able_to_parse(sample_rdf_feed)
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
it "should return false for an rss feedburner feed" do
|
15
|
+
expect(Atom).to_not be_able_to_parse(sample_rss_feed_burner_feed)
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
it "should return true for an atom feed that has line breaks in between attributes in the <feed> node" do # rubocop:disable Metrics/LineLength
|
19
|
+
expect(Atom).to be_able_to_parse(sample_atom_feed_line_breaks)
|
20
|
+
end
|
19
21
|
end
|
20
|
-
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
describe "parsing" do
|
24
|
+
before(:each) do
|
25
|
+
@feed = Atom.parse(sample_atom_feed)
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
it "should parse the title" do
|
29
|
+
expect(@feed.title).to eq "Amazon Web Services Blog"
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
it "should parse the description" do
|
33
|
+
description = "Amazon Web Services, Products, Tools, and Developer Information..." # rubocop:disable Metrics/LineLength
|
34
|
+
expect(@feed.description).to eq description
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
it "should parse the url" do
|
38
|
+
expect(@feed.url).to eq "http://aws.typepad.com/aws/"
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
it "should parse the url even when it doesn't have the type='text/html' attribute" do # rubocop:disable Metrics/LineLength
|
42
|
+
xml = load_sample "atom_with_link_tag_for_url_unmarked.xml"
|
43
|
+
feed = Atom.parse xml
|
44
|
+
expect(feed.url).to eq "http://www.innoq.com/planet/"
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
it "should parse the feed_url even when it doesn't have the type='application/atom+xml' attribute" do # rubocop:disable Metrics/LineLength
|
48
|
+
feed = Atom.parse(load_sample("atom_with_link_tag_for_url_unmarked.xml"))
|
49
|
+
expect(feed.feed_url).to eq "http://www.innoq.com/planet/atom.xml"
|
50
|
+
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
it "should parse the feed_url" do
|
53
|
+
expect(@feed.feed_url).to eq "http://aws.typepad.com/aws/atom.xml"
|
54
|
+
end
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
it "should parse no hub urls" do
|
57
|
+
expect(@feed.hubs.count).to eq 0
|
58
|
+
end
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
it "should parse the hub urls" do
|
61
|
+
feed_with_hub = Atom.parse(load_sample("SamRuby.xml"))
|
62
|
+
expect(feed_with_hub.hubs.count).to eq 1
|
63
|
+
expect(feed_with_hub.hubs.first).to eq "http://pubsubhubbub.appspot.com/"
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
66
|
+
it "should parse entries" do
|
67
|
+
expect(@feed.entries.size).to eq 10
|
68
|
+
end
|
67
69
|
end
|
68
|
-
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
describe "preprocessing" do
|
72
|
+
it "retains markup in xhtml content" do
|
73
|
+
Atom.preprocess_xml = true
|
73
74
|
|
74
|
-
|
75
|
-
|
75
|
+
feed = Atom.parse sample_atom_xhtml_feed
|
76
|
+
entry = feed.entries.first
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
expect(entry.title).to match(/\<i/)
|
79
|
+
expect(entry.summary).to match(/\<b/)
|
80
|
+
expect(entry.content).to match(/\A\<p/)
|
81
|
+
end
|
81
82
|
|
82
|
-
|
83
|
-
|
83
|
+
it "should not duplicate content when there are divs in content" do
|
84
|
+
Atom.preprocess_xml = true
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
|
86
|
+
feed = Atom.parse sample_duplicate_content_atom_feed
|
87
|
+
content = Nokogiri::HTML(feed.entries[1].content)
|
88
|
+
expect(content.css("img").length).to eq 11
|
89
|
+
end
|
88
90
|
end
|
89
|
-
end
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
describe "parsing url and feed url based on rel attribute" do
|
93
|
+
before :each do
|
94
|
+
@feed = Atom.parse(sample_atom_middleman_feed)
|
95
|
+
end
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
97
|
+
it "should parse url" do
|
98
|
+
expect(@feed.url).to eq "http://feedjira.com/blog"
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
-
|
101
|
+
it "should parse feed url" do
|
102
|
+
expect(@feed.feed_url).to eq "http://feedjira.com/blog/feed.xml"
|
103
|
+
end
|
102
104
|
end
|
103
105
|
end
|
104
106
|
end
|