jekyll-ical-tag 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "jekyll"
5
+ require "jekyll-ical-tag/version"
6
+
7
+ module Jekyll
8
+ class IcalTag < Liquid::Block
9
+ require_relative "jekyll-ical-tag/calendar_feed_coordinator"
10
+ require_relative "jekyll-ical-tag/calendar_fetcher"
11
+ require_relative "jekyll-ical-tag/calendar_limiter"
12
+ require_relative "jekyll-ical-tag/calendar_parser"
13
+ require_relative "jekyll-ical-tag/event"
14
+
15
+ include Convertible
16
+
17
+ def initialize(tag_name, markup, parse_context)
18
+ super
19
+ @markup = markup
20
+ @attributes = {}
21
+
22
+ scan_attributes!
23
+ set_limit!
24
+ set_reverse!
25
+ set_url!
26
+ set_only!
27
+ end
28
+
29
+ def render(context)
30
+ context.registers[:ical] ||= Hash.new(0)
31
+
32
+ result = []
33
+
34
+ context.stack do
35
+ url = get_dereferenced_url(context) || @url
36
+ before_date = before_date_from(context)
37
+ after_date = after_date_from(context)
38
+
39
+ calendar_feed_coordinator = CalendarFeedCoordinator.new(
40
+ url: url, only: @only, reverse: @reverse,
41
+ before_date: before_date, after_date: after_date,
42
+ limit: @limit
43
+ )
44
+ events = calendar_feed_coordinator.events
45
+ event_count = events.length
46
+
47
+ events.each_with_index do |event, index|
48
+ # Init
49
+ context["event"] = {}
50
+
51
+ # Jekyll helper variables
52
+ context["event"]["index"] = index
53
+
54
+ # RFC 5545 conformant and custom properties.
55
+ context["event"].merge!(event.all_properties)
56
+
57
+ # Supported but non-standard attributes.
58
+ context["event"]["attendees"] = event.attendees
59
+ context["event"]["simple_html_description"] = event.simple_html_description
60
+
61
+ # Overridden values
62
+ context["event"]["url"] ||= event.description_urls.first
63
+
64
+ # Deprecated attribute names.
65
+ context["event"]["end_time"] = context["event"]["dtend"]
66
+ context["event"]["start_time"] = context["event"]["dtstart"]
67
+
68
+ context["forloop"] = {
69
+ "name" => "ical",
70
+ "length" => event_count,
71
+ "index" => index + 1,
72
+ "index0" => index,
73
+ "rindex" => event_count - index,
74
+ "rindex0" => event_count - index - 1,
75
+ "first" => (index == 0),
76
+ "last" => (index == event_count - 1),
77
+ }
78
+
79
+ result << nodelist.map do |n|
80
+ if n.respond_to? :render
81
+ n.render(context)
82
+ else
83
+ n
84
+ end
85
+ end.join
86
+ end
87
+ end
88
+
89
+ result
90
+ end
91
+
92
+ private
93
+
94
+ def get_dereferenced_url(context)
95
+ return unless context.key?(@url)
96
+
97
+ context[@url]
98
+ end
99
+
100
+ def after_date_from(context)
101
+ safely_cast_to_time(
102
+ dereferenced_liquid_val(context, "after_date")
103
+ ).tap { |v| pp v }
104
+ end
105
+
106
+ def before_date_from(context)
107
+
108
+ safely_cast_to_time(
109
+ dereferenced_liquid_val(context, "before_date")
110
+ ).tap { |v| pp v }
111
+ end
112
+
113
+ def safely_cast_to_time(val)
114
+ case val
115
+ when String
116
+ Time.parse(val)
117
+ when Date, DateTime, Time
118
+ val
119
+ when NilClass
120
+ # Do nothing
121
+ else
122
+ raise "Cannot cast to Time: #{val}"
123
+ end
124
+ end
125
+
126
+ def dereferenced_liquid_val(context, variable_name)
127
+ raw_value = @attributes[variable_name]
128
+
129
+ context.key?(raw_value) ? context[raw_value] : raw_value
130
+ end
131
+
132
+ def scan_attributes!
133
+ @markup.scan(Liquid::TagAttributes) do |key, value|
134
+ @attributes[key] = value
135
+ end
136
+ end
137
+
138
+ def set_limit!
139
+ @limit = nil
140
+ @limit = @attributes["limit"].to_i if @attributes["limit"]
141
+ end
142
+
143
+ def set_reverse!
144
+ @reverse = @attributes["reverse"] == "true"
145
+ end
146
+
147
+ def set_url!
148
+ @url = @attributes["url"]
149
+ end
150
+
151
+ def set_only!
152
+ only_future = @attributes["only_future"] == "true"
153
+ only_past = @attributes["only_past"] == "true"
154
+
155
+ raise "Set only_future OR only_past, not both" if only_future && only_past
156
+
157
+ @only =
158
+ if only_future
159
+ :future
160
+ elsif only_past
161
+ :past
162
+ else
163
+ :all
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ Liquid::Template.register_tag("ical", Jekyll::IcalTag)
@@ -0,0 +1,219 @@
1
+ require "spec_helper"
2
+
3
+ EXAMPLE_RAW_FEEDS = {
4
+ empty: "",
5
+ basic: File.read("spec/support/basic.ics"),
6
+ italian: File.read("spec/support/italian.ics"),
7
+ sesh: File.read("spec/support/sesh.ics")
8
+ }
9
+
10
+ RSpec.describe Jekyll::IcalTag::CalendarFeedCoordinator do
11
+ context "happy path" do
12
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: "https://space.floern.com/launch.ics") }
13
+
14
+ it "should not raise error" do
15
+ expect { coordinator.events }.to_not raise_error
16
+ end
17
+
18
+ it "should return accurate event count" do
19
+ expect(coordinator.events.count).to be > 0
20
+ end
21
+
22
+ it "should be able to parse all parse of each event" do
23
+ coordinator.events.each do |event|
24
+ expect { event.all_properties }.to_not raise_error
25
+ expect { event.simple_html_description }.to_not raise_error
26
+ expect { event.attendees }.to_not raise_error
27
+ expect { event.description_urls }.to_not raise_error
28
+ end
29
+ end
30
+ end
31
+
32
+ context "with empty feed" do
33
+ let(:fake_url) { "https://www.calendarfeed.com/feed.ics"}
34
+ let(:mock_feed) { double(:mock_feed, fetch: EXAMPLE_RAW_FEEDS[:empty])}
35
+
36
+ before do
37
+ allow(Jekyll::IcalTag::CalendarFetcher).to receive(:new).and_return(mock_feed)
38
+ end
39
+
40
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url) }
41
+
42
+ it "should not raise error" do
43
+ expect { coordinator.events }.to_not raise_error
44
+ end
45
+
46
+ it "should return accurate event count" do
47
+ expect(coordinator.events.count).to eq(0)
48
+ end
49
+ end
50
+
51
+ context "with basic feed" do
52
+ let(:fake_url) { "https://www.calendarfeed.com/feed.ics"}
53
+ let(:mock_feed) { double(:mock_feed, fetch: EXAMPLE_RAW_FEEDS[:basic])}
54
+ before { allow(Jekyll::IcalTag::CalendarFetcher).to receive(:new).and_return(mock_feed) }
55
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url) }
56
+
57
+ it "should not raise error" do
58
+ expect {
59
+ coordinator.events
60
+ }.to_not raise_error
61
+ end
62
+
63
+ it "should return accurate event count" do
64
+ expect(coordinator.events.count).to eq(66)
65
+ end
66
+
67
+ it "should be able to parse all parse of each event" do
68
+ coordinator.events.each do |event|
69
+ expect { event.all_properties }.to_not raise_error
70
+ expect { event.simple_html_description }.to_not raise_error
71
+ expect { event.attendees }.to_not raise_error
72
+ expect { event.description_urls }.to_not raise_error
73
+ end
74
+ end
75
+
76
+ it "should return dates from oldest to newest first" do
77
+ first_date = coordinator.events.first.dtstart.to_date
78
+ last_date = coordinator.events.last.dtstart.to_date
79
+
80
+ expect(first_date).to be < last_date
81
+ end
82
+
83
+ describe "limit" do
84
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, limit: 5) }
85
+
86
+ it "should return accurate event count" do
87
+ expect(coordinator.events.count).to eq(5)
88
+ end
89
+ end
90
+
91
+ describe "only future" do
92
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, only: :future) }
93
+
94
+ it "should return accurate event count" do
95
+ expect(coordinator.events.count).to eq(1)
96
+ end
97
+ end
98
+
99
+ describe "only past" do
100
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, only: :past) }
101
+
102
+ it "should return accurate event count" do
103
+ expect(coordinator.events.count).to eq(65)
104
+ end
105
+ end
106
+
107
+ describe "only all" do
108
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, only: :all) }
109
+
110
+ it "should return accurate event count" do
111
+ expect(coordinator.events.count).to eq(66)
112
+ end
113
+ end
114
+
115
+ describe "before date" do
116
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, before_date: Date.parse("1/1/2020")) }
117
+
118
+ it "should return accurate event count" do
119
+ expect(coordinator.events.count).to eq(55)
120
+ end
121
+ end
122
+
123
+ describe "after date" do
124
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, after_date: Date.parse("1/1/2020")) }
125
+
126
+ it "should return accurate event count" do
127
+ expect(coordinator.events.count).to eq(11)
128
+ end
129
+ end
130
+
131
+ describe "reverse" do
132
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, reverse: true) }
133
+
134
+ it "should return dates from oldest to newest first" do
135
+ first_date = coordinator.events.first.dtstart.to_date
136
+ last_date = coordinator.events.last.dtstart.to_date
137
+
138
+ expect(first_date).to be > last_date
139
+ end
140
+ end
141
+ end
142
+
143
+ context "with italian feed" do
144
+ let(:fake_url) { "https://www.calendarfeed.com/feed.ics"}
145
+ let(:mock_feed) { double(:mock_feed, fetch: EXAMPLE_RAW_FEEDS[:italian])}
146
+ before { allow(Jekyll::IcalTag::CalendarFetcher).to receive(:new).and_return(mock_feed) }
147
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url) }
148
+
149
+ it "should not raise error" do
150
+ expect {
151
+ coordinator.events
152
+ }.to_not raise_error
153
+ end
154
+
155
+ it "should return accurate event count" do
156
+ expect(coordinator.events.count).to eq(10)
157
+ end
158
+
159
+ it "should be able to parse all parse of each event" do
160
+ coordinator.events.each do |event|
161
+ expect { event.all_properties }.to_not raise_error
162
+ expect { event.simple_html_description }.to_not raise_error
163
+ expect { event.attendees }.to_not raise_error
164
+ expect { event.description_urls }.to_not raise_error
165
+ end
166
+ end
167
+
168
+ it "selected outputs should always be strings" do
169
+ coordinator.events.each do |event|
170
+ expect(event.simple_html_description.to_s).to be_a String
171
+ expect(event.description.to_s).to be_a String
172
+ event.all_properties.each do |property, value|
173
+ expect(value).to be_a(Time)
174
+ .or be_a(Date)
175
+ .or be_a(String)
176
+ .or be_a(NilClass)
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+
183
+ context "with sesh feed" do
184
+ let(:fake_url) { "https://www.calendarfeed.com/feed.ics"}
185
+ let(:mock_feed) { double(:mock_feed, fetch: EXAMPLE_RAW_FEEDS[:sesh])}
186
+ before { allow(Jekyll::IcalTag::CalendarFetcher).to receive(:new).and_return(mock_feed) }
187
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url) }
188
+
189
+ it "should return accurate event count" do
190
+ expect(coordinator.events.count).to eq(3)
191
+ end
192
+
193
+ describe "reverse" do
194
+ let(:coordinator) { Jekyll::IcalTag::CalendarFeedCoordinator.new(url: fake_url, reverse: reverse) }
195
+
196
+ context "when reversed" do
197
+ let(:reverse) { true }
198
+
199
+ it "should return dates from oldest to newest first" do
200
+ first_date = coordinator.events.first.dtstart.to_date
201
+ last_date = coordinator.events.last.dtstart.to_date
202
+
203
+ expect(first_date).to be > last_date
204
+ end
205
+ end
206
+
207
+ context "when not reversed" do
208
+ let(:reverse) { false }
209
+
210
+ it "should return dates from oldest to newest first" do
211
+ first_date = coordinator.events.first.dtstart.to_date
212
+ last_date = coordinator.events.last.dtstart.to_date
213
+
214
+ expect(first_date).to be < last_date
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,85 @@
1
+ require "rspec"
2
+
3
+ require "./lib/jekyll-ical-tag"
4
+
5
+ RSpec.configure do |config|
6
+ # rspec-expectations config goes here. You can use an alternate
7
+ # assertion/expectation library such as wrong or the stdlib/minitest
8
+ # assertions if you prefer.
9
+ config.expect_with :rspec do |expectations|
10
+ # This option will default to `true` in RSpec 4. It makes the `description`
11
+ # and `failure_message` of custom matchers include text for helper methods
12
+ # defined using `chain`, e.g.:
13
+ # be_bigger_than(2).and_smaller_than(4).description
14
+ # # => "be bigger than 2 and smaller than 4"
15
+ # ...rather than:
16
+ # # => "be bigger than 2"
17
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
18
+ end
19
+
20
+ # rspec-mocks config goes here. You can use an alternate test double
21
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
22
+ config.mock_with :rspec do |mocks|
23
+ # Prevents you from mocking or stubbing a method that does not exist on
24
+ # a real object. This is generally recommended, and will default to
25
+ # `true` in RSpec 4.
26
+ mocks.verify_partial_doubles = true
27
+ end
28
+
29
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
30
+ # have no way to turn it off -- the option exists only for backwards
31
+ # compatibility in RSpec 3). It causes shared context metadata to be
32
+ # inherited by the metadata hash of host groups and examples, rather than
33
+ # triggering implicit auto-inclusion in groups with matching metadata.
34
+ config.shared_context_metadata_behavior = :apply_to_host_groups
35
+
36
+ # This allows you to limit a spec run to individual examples or groups
37
+ # you care about by tagging them with `:focus` metadata. When nothing
38
+ # is tagged with `:focus`, all examples get run. RSpec also provides
39
+ # aliases for `it`, `describe`, and `context` that include `:focus`
40
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
41
+ config.filter_run_when_matching :focus
42
+
43
+ # Allows RSpec to persist some state between runs in order to support
44
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
45
+ # you configure your source control system to ignore this file.
46
+ config.example_status_persistence_file_path = "spec/examples.txt"
47
+
48
+ # Limits the available syntax to the non-monkey patched syntax that is
49
+ # recommended. For more details, see:
50
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
51
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
52
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
53
+ config.disable_monkey_patching!
54
+
55
+ # This setting enables warnings. It"s recommended, but in some cases may
56
+ # be too noisy due to issues in dependencies.
57
+ # config.warnings = true
58
+
59
+ # Many RSpec users commonly either run the entire suite or an individual
60
+ # file, and it"s useful to allow more verbose output when running an
61
+ # individual spec file.
62
+ if config.files_to_run.one?
63
+ # Use the documentation formatter for detailed output,
64
+ # unless a formatter has already been configured
65
+ # (e.g. via a command-line flag).
66
+ config.default_formatter = "doc"
67
+ end
68
+
69
+ # Print the 10 slowest examples and example groups at the
70
+ # end of the spec run, to help surface which specs are running
71
+ # particularly slow.
72
+ config.profile_examples = 10
73
+
74
+ # Run specs in random order to surface order dependencies. If you find an
75
+ # order dependency and want to debug it, you can fix the order by providing
76
+ # the seed, which is printed after each run.
77
+ # --seed 1234
78
+ config.order = :random
79
+
80
+ # Seed global randomization in this process using the `--seed` CLI option.
81
+ # Setting this allows you to use `--seed` to deterministically reproduce
82
+ # test failures related to randomization by passing the same `--seed` value
83
+ # as the one that triggered the failure.
84
+ Kernel.srand config.seed
85
+ end