jekyll-paginate-v3 0.2.0.alpha → 0.2.1.alpha

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 800113e7a994aa5e6b1a85ec42a7af7a2d14af9c47e8ad9db98de54763b4b6db
4
- data.tar.gz: d1e054fdd4621e09fb37f36d98c7009d6d8891e12ce485ef3d6fc0bb3aa7aa99
3
+ metadata.gz: e41115e64f970d6f2d2daf447c81230cb2550ed6d38ac462e96c11a10681b7d3
4
+ data.tar.gz: 54778efed89955bd2095392a43f35e19958210ed4cb3d87855487087d3d8d81d
5
5
  SHA512:
6
- metadata.gz: 9a955153959b071277e958f5b6d24518a49295b401edd2fc15ee74f282e85e4f3ca5af6ae1be61a5b8af0c72b2a9163d49e6ee4214df11886609c56c29363e9b
7
- data.tar.gz: 56c0a857ce7e8275c5f4081b427bf6dd6e79ff2ab571cb6446a4d8901c5cc0229c416ed263a9ed497d1737149556dc181b8ef37337209fec743dfa347287f776
6
+ metadata.gz: cf2d428b82e079b9435959f1d58ba2da88464beb23fe0cc7d3e4c89f539e1abc93e7c4ebab825aace5f41424a37ec15111549fdc0b3f905024e7a853b7cf5939
7
+ data.tar.gz: ba8014531b022ea9bf76f2b078483f890d7725b3f2a118f1a0d675763b0a12a2a80589888ae4c80ccfe70077b27efeb9ba557f788e1cb8d09ed7aecf3a6a84ec
@@ -150,7 +150,7 @@ class Filter
150
150
 
151
151
  # Checks one item value against an optional min/max range.
152
152
  def range_match?(value, min_value, max_value, range_mode)
153
- comparable_value = normalise_comparable_scalar(value)
153
+ comparable_value = normalise_range_candidate(value, min_value, max_value)
154
154
  return false if comparable_value.nil?
155
155
 
156
156
  min_inclusive, max_inclusive = parse_range_mode_flags(range_mode)
@@ -178,6 +178,18 @@ class Filter
178
178
  false
179
179
  end
180
180
 
181
+ # Coerces a range candidate according to its already-normalised boundary
182
+ # type. Date parsing is therefore explicit to range evaluation and never
183
+ # leaks into ordinary scalar equality.
184
+ def normalise_range_candidate(value, min_value, max_value)
185
+ boundary = min_value.nil? ? max_value : min_value
186
+ if boundary.is_a?(Date) || boundary.is_a?(DateTime) || boundary.is_a?(Time)
187
+ return Jekyll::Plugins::PaginateV3::Support::LooseScalar.datetime(value)
188
+ end
189
+
190
+ Jekyll::Plugins::PaginateV3::Support::LooseScalar.number(value)
191
+ end
192
+
181
193
  # Parses one canonical range mode string into inclusion flags.
182
194
  def parse_range_mode_flags(range_mode)
183
195
  mode_text = range_mode.to_s
@@ -531,7 +531,13 @@ class Filter
531
531
  return today_expression unless today_expression.nil?
532
532
  end
533
533
 
534
- interpret_numeric(value, must_cast: true)
534
+ numeric_value = Jekyll::Plugins::PaginateV3::Support::LooseScalar.number(value)
535
+ return numeric_value unless numeric_value.nil?
536
+
537
+ datetime_value = Jekyll::Plugins::PaginateV3::Support::LooseScalar.datetime(value)
538
+ return datetime_value unless datetime_value.nil?
539
+
540
+ false
535
541
  end
536
542
 
537
543
  # Parses configured now-keyword expressions into DateTime values.
@@ -572,7 +578,7 @@ class Filter
572
578
  end
573
579
  end
574
580
 
575
- # Casts string values to Integer, Float, or DateTime when possible.
581
+ # Casts string values to Integer or Float when possible.
576
582
  # Returns the original string unless strict casting is requested.
577
583
  def interpret_numeric(value, must_cast: false)
578
584
  comparable_value = Jekyll::Plugins::PaginateV3::Support::LooseScalar.comparable(value, must_cast: must_cast)
@@ -12,6 +12,42 @@ module Support
12
12
  # Structure: callers can ask for one specific scalar family without
13
13
  # committing the whole pipeline to one feature's semantics.
14
14
  module LooseScalar
15
+ # Captures the supported ISO-shaped date/time profile without exposing
16
+ # callers to Ruby's permissive and time-dependent `DateTime.parse` rules.
17
+ # Date separators repeat independently of the time separator, which also
18
+ # repeats inside a numeric timezone offset.
19
+ DATETIME_STRING_PATTERN = %r{
20
+ \A
21
+ (?<year>\d{4})
22
+ (?<date_separator>[:.\/-])
23
+ (?<month>\d{2})
24
+ \k<date_separator>
25
+ (?<day>\d{2})
26
+ (?:
27
+ (?:T|[ \t]+)
28
+ (?<hour>\d{2})
29
+ (?<time_separator>[:.-])
30
+ (?<minute>\d{2})
31
+ (?:
32
+ \k<time_separator>
33
+ (?<second>\d{2})
34
+ (?<fraction>\.\d+)?
35
+ )?
36
+ (?:
37
+ [ \t]*
38
+ (?:
39
+ (?<utc_timezone>Z)
40
+ |
41
+ (?<timezone_sign>[+-])
42
+ (?<timezone_hour>\d{2})
43
+ \k<time_separator>
44
+ (?<timezone_minute>\d{2})
45
+ )
46
+ )?
47
+ )?
48
+ \z
49
+ }x.freeze
50
+
15
51
  # Interprets one value as a strict boolean or a loose `true`/`false`
16
52
  # string. Returns `nil` when no boolean meaning can be inferred.
17
53
  def self.boolean(value)
@@ -50,8 +86,9 @@ module LooseScalar
50
86
  (numeric_value % 1).zero?
51
87
  end
52
88
 
53
- # Interprets one value as a datetime in the same loose way used by
54
- # filter range comparisons.
89
+ # Interprets one value as a datetime for explicitly date-aware operations.
90
+ # Strings must satisfy `DATETIME_STRING_PATTERN`; captured components are
91
+ # rebuilt as canonical ISO before the standard parser validates them.
55
92
  def self.datetime(value)
56
93
  return value if value.is_a?(DateTime)
57
94
  return value.to_datetime if value.is_a?(Time)
@@ -61,7 +98,11 @@ module LooseScalar
61
98
  stripped = value.strip
62
99
  return nil if stripped.empty?
63
100
 
64
- DateTime.parse(stripped)
101
+ match = DATETIME_STRING_PATTERN.match(stripped)
102
+ return nil if match.nil?
103
+ return nil unless valid_datetime_components?(match)
104
+
105
+ DateTime.iso8601(canonical_datetime(match))
65
106
  rescue ArgumentError
66
107
  nil
67
108
  end
@@ -71,9 +112,9 @@ module LooseScalar
71
112
  datetime(value)
72
113
  end
73
114
 
74
- # Converts one value into the loose comparable scalar form used by
75
- # filtering. Strings become numbers or datetimes when possible, or a
76
- # stripped string otherwise.
115
+ # Converts one value into the generic comparable scalar form used by
116
+ # exact filtering. Strings become numbers when possible, but date-shaped
117
+ # strings remain strings unless a date-aware caller invokes `datetime`.
77
118
  def self.comparable(value, must_cast: false)
78
119
  return value if value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(DateTime)
79
120
  return value.to_datetime if value.is_a?(Time)
@@ -90,14 +131,48 @@ module LooseScalar
90
131
  numeric_value = number(stripped)
91
132
  return numeric_value unless numeric_value.nil?
92
133
 
93
- datetime_value = datetime(stripped)
94
- return datetime_value unless datetime_value.nil?
95
-
96
134
  return nil if must_cast
97
135
 
98
136
  stripped
99
137
  end
100
138
 
139
+ # Rebuilds one lexical match as strict ISO text for calendar validation.
140
+ def self.canonical_datetime(match)
141
+ date = "#{match[:year]}-#{match[:month]}-#{match[:day]}"
142
+ return date if match[:hour].nil?
143
+
144
+ second = match[:second] || '00'
145
+ fraction = match[:fraction].to_s
146
+ timezone = canonical_timezone(match)
147
+
148
+ "#{date}T#{match[:hour]}:#{match[:minute]}:#{second}#{fraction}#{timezone}"
149
+ end
150
+ private_class_method :canonical_datetime
151
+
152
+ # Converts an optional loose timezone into canonical ISO syntax. Missing
153
+ # timezone information deliberately means offset zero rather than local time.
154
+ def self.canonical_timezone(match)
155
+ return 'Z' unless match[:utc_timezone].nil?
156
+ return 'Z' if match[:timezone_sign].nil?
157
+
158
+ "#{match[:timezone_sign]}#{match[:timezone_hour]}:#{match[:timezone_minute]}"
159
+ end
160
+ private_class_method :canonical_timezone
161
+
162
+ # Validates component ranges explicitly because `DateTime.iso8601` accepts
163
+ # and normalises some out-of-range timezone offsets on older Ruby versions.
164
+ def self.valid_datetime_components?(match)
165
+ return false unless Date.valid_date?(match[:year].to_i, match[:month].to_i, match[:day].to_i)
166
+ return true if match[:hour].nil?
167
+ return false unless (0..23).cover?(match[:hour].to_i)
168
+ return false unless (0..59).cover?(match[:minute].to_i)
169
+ return false unless match[:second].nil? || (0..59).cover?(match[:second].to_i)
170
+ return true if match[:timezone_sign].nil?
171
+
172
+ (0..23).cover?(match[:timezone_hour].to_i) && (0..59).cover?(match[:timezone_minute].to_i)
173
+ end
174
+ private_class_method :valid_datetime_components?
175
+
101
176
  # Safe comparability check for mixed scalar types.
102
177
  def self.comparable_values?(left, right)
103
178
  return true if left.class == right.class
@@ -252,7 +252,10 @@ class GroupedIndex
252
252
  keyword_value = interpret_datetime_keyword_expression(stripped, range_key: 'min')
253
253
  return keyword_value unless keyword_value.nil?
254
254
 
255
- DateTime.parse(stripped)
255
+ datetime_value = Jekyll::Plugins::PaginateV3::Support::LooseScalar.datetime(stripped)
256
+ return datetime_value unless datetime_value.nil?
257
+
258
+ raise ArgumentError
256
259
  rescue ArgumentError
257
260
  raise ArgumentError, "`group.start` could not be parsed as datetime: #{raw_value.inspect}."
258
261
  end
@@ -355,10 +358,10 @@ class GroupedIndex
355
358
  return true if stripped.match?(/\A(?:#{precise_units})(?:\s*\(|\z)/i)
356
359
  return true if stripped.match?(/\A#{Regexp.escape(@now_keyword)}(?:\s*[+-]\s*\d+)?\z/i)
357
360
 
358
- parsed = DateTime.parse(stripped)
361
+ parsed = Jekyll::Plugins::PaginateV3::Support::LooseScalar.datetime(stripped)
362
+ return false if parsed.nil?
363
+
359
364
  parsed.hour.positive? || parsed.min.positive? || parsed.sec.positive?
360
- rescue ArgumentError
361
- false
362
365
  end
363
366
  end
364
367
 
@@ -4,7 +4,7 @@ module Jekyll
4
4
  module Plugins
5
5
 
6
6
  module PaginateV3
7
- VERSION = '0.2.0.alpha'
7
+ VERSION = '0.2.1.alpha'
8
8
  end
9
9
 
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-paginate-v3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.alpha
4
+ version: 0.2.1.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Convincible
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-08 00:00:00.000000000 Z
11
+ date: 2026-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll