citeproc 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9053957ce407de4b219008b6f732e9037a6bdec99dc4e04792bb8ca4f46f9f42
4
- data.tar.gz: 96e2cbde7609f0fc9aa2c52d972a7ab51e16bce2cb3412d7042c02f32a47c08f
3
+ metadata.gz: 8708658d44ed66deb17744c846697187edbcd1f6dc308c4bf3bc69933ede3e7e
4
+ data.tar.gz: 1d1af4037b6dae14f20adffdbb7e51b35ecd02728586f95932348416788347c1
5
5
  SHA512:
6
- metadata.gz: b2e50088fd201c2546614cb5e90e6862d4b4b07ec3b5a87262b9bd70cfd453e1dc051ffaaa02379e10b709da5f5c455eca61b003b31049165a9ebf8f79f7ba63
7
- data.tar.gz: 636492e1a454f7f6a3ade9c30c1346cd036df0067be69b3d4102cf0b590df0959aa6dab6a619bd17333512ab6723c4d3a6823bc66fe6b334c2e9a001e5fffcae
6
+ metadata.gz: 4dc2d276583d4071cf17459265a3c706f8eb4091c59527a80506241e8f5187de4eba9f6cb140e560327fde38f8e8225348818dfeda0e1b2b98ef53acb67c5064
7
+ data.tar.gz: 33de631e29bd0e3484a0509e4f61bfb4e2b59fe87aeb8e7cbd25c1d9065ce20588637725f00d9f88ab2e19bcee765d3a19bc71d25adced9135d6a74627018279
data/README.md CHANGED
@@ -86,10 +86,9 @@ You can speed up the XML parsing by installing
86
86
  [Nokogiri](https://rubygems.org/gems/nokogiri);
87
87
  otherwise the REXML from the Ruby standard library will be used.
88
88
 
89
- Similarly, you can install either of the gems
90
- [EDTF](https://rubygems.org/gems/edtf) or
91
- [Chronic](https://rubygems.org/gems/chronic)
92
- to support a wide range of additional inputs for date variables.
89
+ Similarly, you can install the
90
+ [EDTF](https://rubygems.org/gems/edtf)
91
+ gem to support a wide range of additional inputs for date variables.
93
92
 
94
93
  CSL Styles and Locales
95
94
  ----------------------
@@ -37,7 +37,7 @@ module CiteProc
37
37
  return false if value.nil?
38
38
  return false if value.respond_to?(:empty?) && value.empty?
39
39
 
40
- value.to_s !~ /^(false|no|never)$/i
40
+ value.to_s !~ /^(false|no|never|0)$/i
41
41
  end
42
42
 
43
43
  def filter_key(key)
@@ -108,7 +108,7 @@ module CiteProc
108
108
  case
109
109
  when equal?(other)
110
110
  true
111
- when self.class != other.class, length != other.length
111
+ when self.class != other.class, attributes.length != other.attributes.length
112
112
  false
113
113
  else
114
114
  other.attributes.each_pair do |key, value|
@@ -193,7 +193,7 @@ module CiteProc
193
193
  end
194
194
 
195
195
  def hanging_indent?
196
- options[:'hanging_indent']
196
+ CiteProc.boolean(options[:'hanging-indent'])
197
197
  end
198
198
 
199
199
  def join()
@@ -11,12 +11,72 @@ module CiteProc
11
11
  include Comparable
12
12
 
13
13
  @labels = [
14
- :book, :chapter, :column, :figure, :folio, :issue, :line, :note, :opus,
15
- :page, :paragraph, :part, :section, :'sub-verbo', :verse, :volume
14
+ :act,
15
+ :appendix,
16
+ :'article-locator',
17
+ :book,
18
+ :canon,
19
+ :chapter,
20
+ :column,
21
+ :elocation,
22
+ :equation,
23
+ :figure,
24
+ :folio,
25
+ :issue,
26
+ :line,
27
+ :note,
28
+ :opus,
29
+ :page,
30
+ :paragraph,
31
+ :part,
32
+ :rule,
33
+ :scene,
34
+ :section,
35
+ :'sub-verbo',
36
+ :supplement,
37
+ :table,
38
+ :timestamp,
39
+ :'title-locator',
40
+ :verse,
41
+ :version,
42
+ :volume
16
43
  ].freeze
17
44
 
45
+ # Abbreviations of locator labels (for extraction)
46
+ @locator_abbreviations = {
47
+ 'app.' => 'appendix',
48
+ 'art.' => 'article-locator',
49
+ 'bk.' => 'book',
50
+ 'ch.' => 'chapter',
51
+ 'col.' => 'column',
52
+ 'fig.' => 'figure',
53
+ 'fol.' => 'folio',
54
+ 'l.' => 'line',
55
+ 'n.' => 'note',
56
+ 'no.' => 'issue',
57
+ 'op.' => 'opus',
58
+ 'p.' => 'page',
59
+ 'pp.' => 'page',
60
+ 'para.' => 'paragraph',
61
+ 'pt.' => 'part',
62
+ 'r.' => 'rule',
63
+ 'sec.' => 'section',
64
+ 'supp.' => 'supplement',
65
+ 'sv.' => 'sub-verbo',
66
+ 'tbl.' => 'table',
67
+ 'tit.' => 'title-locator',
68
+ 'vol.' => 'volume',
69
+ 'vrs.' => 'verse'
70
+ }.freeze
71
+
18
72
  class << self
19
- attr_reader :labels
73
+ attr_reader :labels, :locator_abbreviations
74
+
75
+ # @param abbreviations [Hash<String,String>] label abbreviations
76
+ # @return [Regexp] a pattern matching any of the abbreviations
77
+ def locator_label_pattern(abbreviations = locator_abbreviations)
78
+ Regexp.union(abbreviations.keys.sort_by { |abbr| -abbr.length })
79
+ end
20
80
  end
21
81
 
22
82
  # @!attribute id
@@ -66,6 +126,32 @@ module CiteProc
66
126
  yield self if block_given?
67
127
  end
68
128
 
129
+ # @return [String, nil] the locator label;
130
+ # defaults to "page" if there is a locator
131
+ def label
132
+ read_attribute(:label) || ('page' if locator?)
133
+ end
134
+
135
+ # Moves a label at the start of the locator into the label
136
+ # (e.g., "vol. 1" becomes "1" with the label "volume") unless
137
+ # the item has a label other than "page".
138
+ #
139
+ # @param abbreviations [Hash<String,String>] label abbreviations
140
+ # @return [self]
141
+ def parse_locator!(abbreviations = CitationItem.locator_abbreviations)
142
+ return self unless locator? && label.to_s == 'page'
143
+
144
+ pattern = CitationItem.locator_label_pattern(abbreviations)
145
+ match = /\A(#{pattern})\s+(.+)\z/m.match(locator.to_s)
146
+
147
+ unless match.nil?
148
+ self.label = abbreviations[match[1]]
149
+ self.locator = match[2]
150
+ end
151
+
152
+ self
153
+ end
154
+
69
155
  def initialize_copy(other)
70
156
  @attributes = other.attributes.deep_copy
71
157
  end
@@ -83,8 +169,6 @@ module CiteProc
83
169
  end
84
170
 
85
171
 
86
-
87
-
88
172
  class CitationData
89
173
 
90
174
  extend Forwardable
data/lib/citeproc/date.rb CHANGED
@@ -7,8 +7,8 @@ module CiteProc
7
7
  #
8
8
  # {Date Dates} can be constructed from a wide range of input values,
9
9
  # including Ruby date objects, integers, date ranges, ISO 8601 and
10
- # CiteProc JSON strings or hashes, and - provided you have the respective
11
- # gems installed - EDTF strings all strings supported by Chronic.
10
+ # CiteProc JSON strings or hashes, and - provided you have the EDTF gem
11
+ # installed - EDTF strings.
12
12
  #
13
13
  # @example Initialization
14
14
  # CiteProc::Date.new
@@ -17,9 +17,6 @@ module CiteProc
17
17
  # CiteProc::Date.today
18
18
  # #-> #<CiteProc::Date "[2012, 6, 10]">
19
19
  #
20
- # CiteProc::Date.new('Yesterday')
21
- # #-> #<CiteProc::Date "[[2012, 6, 9]]">
22
- #
23
20
  # CiteProc::Date.new(1966)
24
21
  # #-> #<CiteProc::Date "[1966]">
25
22
  #
@@ -129,6 +126,18 @@ module CiteProc
129
126
  !bc? && year < 1000
130
127
  end
131
128
 
129
+ # Seasons may be encoded as months 21 to 24 (Spring to Winter).
130
+ # @return [Integer, nil] the season (1 to 4) or nil if the
131
+ # month does not encode a season
132
+ def season
133
+ month - 20 if month && month.between?(21, 24)
134
+ end
135
+
136
+ # @return [Boolean] whether or not the month encodes a season
137
+ def season?
138
+ !season.nil?
139
+ end
140
+
132
141
  # Formats the date parts according to the passed-in format string.
133
142
  # @param format [String] a format string
134
143
  # @return [String,nil] the formatted date string; nil if the date
@@ -148,8 +157,10 @@ module CiteProc
148
157
  # @return [Fixnum,nil] the result of the comparison (-1, 0, 1 or nil)
149
158
  def <=>(other)
150
159
  case
160
+ when other.is_a?(DateParts) && season? && other.season?
161
+ [year, season] <=> [other.year, other.season]
151
162
  when other.is_a?(DateParts)
152
- to_citeproc <=> other.to_citeproc
163
+ comparable <=> other.comparable
153
164
  when other.respond_to?(:to_date)
154
165
  to_date <=> other.to_date
155
166
  else
@@ -192,6 +203,14 @@ module CiteProc
192
203
  def inspect
193
204
  "#<DateParts #{to_s}>"
194
205
  end
206
+
207
+ protected
208
+
209
+ # Seasons are compared like dates without month.
210
+ # @return [Array<Fixnum>] the date parts used for comparison
211
+ def comparable
212
+ season? ? to_citeproc.take(1) : to_citeproc
213
+ end
195
214
  end
196
215
 
197
216
  #
@@ -210,13 +229,11 @@ module CiteProc
210
229
  # List of date parsers (must respond to #parse)
211
230
  @parsers = []
212
231
 
213
- [%w{ edtf EDTF }, %w{ chronic Chronic }].each do |date_parser, module_id|
214
- begin
215
- require date_parser
216
- @parsers << ::Object.const_get(module_id)
217
- rescue LoadError
218
- # warn "failed to load `#{date_parser}' gem"
219
- end
232
+ begin
233
+ require 'edtf'
234
+ @parsers << ::EDTF
235
+ rescue LoadError
236
+ # warn "failed to load `edtf' gem"
220
237
  end
221
238
 
222
239
  @parsers << ::Date
@@ -229,9 +246,8 @@ module CiteProc
229
246
  # A list of available date parsers. Each parser must respond to a
230
247
  # #parse method that converts a date string into a Ruby date object.
231
248
  # By default, the list will include Ruby's date parser from the
232
- # standard library, as well as the parsers of the Chronic and EDTF
233
- # gems if they are available; to install the latter on your system
234
- # make sure to `gem install chronic edtf`.
249
+ # standard library, as well as the parser of the EDTF gem if it is
250
+ # available; to install it on your system run `gem install edtf`.
235
251
  #
236
252
  # @return [Array] the available date parsers
237
253
  attr_reader :parsers
@@ -276,12 +292,44 @@ module CiteProc
276
292
  end
277
293
 
278
294
 
279
- # Make Date behave like a regular Ruby Date
280
- def_delegators :to_ruby, *::Date.instance_methods(false).reject { |m|
281
- m.to_s =~ /^[\W_]|[!=_]$|^(to_s|inspect|dup|clone|change)$|^(marshal|season|year|month|day|certain|uncertain)/
282
- }
295
+ def_delegators :to_ruby, :strftime
296
+
297
+ attr_predicates :circa, :literal, :'date-parts'
298
+
299
+ # @return [Boolean] whether or not the date has a season field
300
+ def has_season?
301
+ attribute?(:season)
302
+ end
303
+
304
+ def season=(season)
305
+ write_attribute :season, season
306
+ end
307
+
308
+ # Season names recognized in the season field
309
+ SEASONS = {
310
+ 'spring' => 1, 'summer' => 2, 'autumn' => 3, 'fall' => 3, 'winter' => 4
311
+ }.freeze
312
+
313
+ # A season takes the place of the month. If the (start) date has a
314
+ # month, the season is encoded by the month (21 to 24); otherwise
315
+ # the season field is used. Season numbers (1 to 4) and names in
316
+ # the season field are converted to integers.
317
+ #
318
+ # @return [Integer, String, nil] the season (1 to 4), the season
319
+ # field as a string if it is not a season number or name, or nil
320
+ def season
321
+ return parts[0].season if month
283
322
 
284
- attr_predicates :circa, :season, :literal, :'date-parts'
323
+ value = read_attribute(:season) or return
324
+ name = value.to_s.strip.downcase
325
+
326
+ SEASONS[name] || (name.match?(/\A[1-4]\z/) ? name.to_i : value.to_s)
327
+ end
328
+
329
+ # @return [Boolean] whether or not the date has a season (see #season)
330
+ def season?
331
+ !season.nil?
332
+ end
285
333
 
286
334
  def initialize(value = {})
287
335
  super
@@ -321,11 +369,15 @@ module CiteProc
321
369
  when value.is_a?(Hash)
322
370
  attributes = value.symbolize_keys
323
371
 
324
- if attributes.has_key?(:raw)
325
- @value = Date.parse(attributes.delete(:raw)).value
372
+ if attributes.has_key?(:raw) && Array(attributes[:'date-parts']).empty?
373
+ raw = attributes.delete(:raw)
374
+ attributes.delete(:'date-parts')
375
+ date = Date.parse(raw)
376
+
377
+ @value = date.nil? ? { :literal => raw } : date.value
326
378
  @value.merge!(attributes)
327
379
  else
328
- @value = attributes.deep_copy
380
+ @value = attributes.except(:raw).deep_copy
329
381
  end
330
382
  convert_parts!
331
383
 
@@ -397,34 +449,16 @@ module CiteProc
397
449
  d
398
450
  end
399
451
 
400
- # @return [::Date,nil] the date (start date if this instance is a range); or nil
401
- def start_date
402
- d = parts[0] and d.to_date
403
- end
404
-
405
- def start_date=(date)
406
- parts[0] = DateParts.new(date.strftime('%Y-%m-%d').split(/-/))
407
- end
408
-
409
- def end_date=(date)
410
- parts[1] = DateParts.new(date.nil? ? 0 : date.strftime('%Y-%m-%d').split(/-/))
411
- end
412
-
413
- # @return [Date,Range] the date as a Ruby date object or as a Range if
414
- # this instance is closed range
452
+ # @return [::Date, Range, nil] the date as a Ruby date object or as
453
+ # a Range if this instance is closed range
415
454
  def to_ruby
416
455
  if closed_range?
417
- start_date..end_date
456
+ parts[0].to_date..parts[1].to_date
418
457
  else
419
- start_date
458
+ d = parts[0] and d.to_date
420
459
  end
421
460
  end
422
461
 
423
- # @return [::Date,nil] the range's end date; or nil
424
- def end_date
425
- d = parts[1] and d.to_date
426
- end
427
-
428
462
  # @return [Boolean] whether or not the date-parts contain an end date
429
463
  def has_end_date?
430
464
  parts[1] && !parts[1].empty?
@@ -506,7 +540,7 @@ module CiteProc
506
540
  case
507
541
  when literal?
508
542
  literal
509
- when season?
543
+ when has_season?
510
544
  season
511
545
  else
512
546
  parts.map(&:to_citeproc).inspect
@@ -516,8 +550,14 @@ module CiteProc
516
550
  def <=>(other)
517
551
  case other
518
552
  when CiteProc::Date
519
- return nil if season? || other.season?
520
- parts <=> other.parts
553
+ result = parts <=> other.parts
554
+
555
+ # Seasons are ordered among themselves only
556
+ if result == 0 && season? && other.season?
557
+ result = season.to_i <=> other.season.to_i
558
+ end
559
+
560
+ result
521
561
  when ::Date
522
562
  parts <=> [other]
523
563
  else
@@ -92,20 +92,19 @@ module CiteProc
92
92
  end
93
93
 
94
94
  # Names quack sorta like a String
95
- def_delegators :to_s, :=~, :===, *String.instance_methods(false).reject { |m|
96
- m.to_s =~ /^[\W_]|[!=_]$|^(to_s|inspect|replace|first|last|dup|clone)$/
97
- }
95
+ def_delegators :to_s,
96
+ :to_str,
97
+ :=~, :===, :match, :match?, :start_with?, :end_with?, :include?,
98
+ :upcase, :downcase, :capitalize, :sub, :gsub, :split, :strip
98
99
 
99
- # Delegate bang! methods to each field's value
100
- String.instance_methods(false).each do |m|
101
- if m.to_s.end_with?('!')
102
- define_method(m) do |*arguments, &block|
103
- Name.parts.each do |part|
104
- p = attributes[part]
105
- p.send(m, *arguments, &block) if p.respond_to?(m)
106
- end
107
- self
100
+ # Delegate in-place transformations to each field's value
101
+ [:gsub!, :sub!, :upcase!, :downcase!, :capitalize!, :strip!].each do |m|
102
+ define_method(m) do |*arguments, &block|
103
+ Name.parts.each do |part|
104
+ p = attributes[part]
105
+ p.send(m, *arguments, &block) if p.respond_to?(m)
108
106
  end
107
+ self
109
108
  end
110
109
  end
111
110
 
@@ -236,7 +235,7 @@ module CiteProc
236
235
 
237
236
  # @return [Boolean] whether or not initials will be used for printing
238
237
  def initials?
239
- !!options[:'initialize-with'] && personal? && romanesque?
238
+ !!options[:'initialize-with'] && personal? && family? && romanesque?
240
239
  end
241
240
 
242
241
  def initialize_with
@@ -244,7 +243,7 @@ module CiteProc
244
243
  end
245
244
 
246
245
  def initialize_existing_only?
247
- options[:initialize].to_s == 'false'
246
+ !CiteProc.boolean(options[:initialize], true)
248
247
  end
249
248
 
250
249
  def initialize_without_hyphen?
@@ -259,8 +258,6 @@ module CiteProc
259
258
  case
260
259
  when !initials?
261
260
  given
262
- when initialize_existing_only?
263
- existing_initials_of given
264
261
  else
265
262
  initials_of given
266
263
  end
@@ -321,7 +318,7 @@ module CiteProc
321
318
  when literal?
322
319
  literal.to_s
323
320
  when static_order?
324
- [family, initials].compact.join(' ')
321
+ [family, initials].compact.join(romanesque? ? ' ' : '')
325
322
  when !short_form?
326
323
  case
327
324
  when !sort_order?
@@ -356,7 +353,7 @@ module CiteProc
356
353
 
357
354
  # @return [String] the name as a string stripped off all markup
358
355
  def strip_markup
359
- gsub(Variable.markup, '')
356
+ to_s.gsub(Variable.markup, '')
360
357
  end
361
358
 
362
359
  # @return [self] the name with all parts stripped off markup
@@ -384,36 +381,30 @@ module CiteProc
384
381
 
385
382
  def initials_of(string)
386
383
  return unless string
387
- string = string.dup
388
-
389
- string.gsub!(/([[:upper:]])[^[:upper:]\s-]*\s*/, "\\1#{initialize_with}")
390
384
 
391
- initialize_hyphen!(string)
392
-
393
- string.strip!
394
- string
395
- end
396
-
397
- def initialize_hyphen!(string)
398
- if initialize_without_hyphen?
399
- string.tr!('-', '')
400
- else
401
- string.gsub!(/\s*-/, '-')
385
+ # Split abbreviations like "M.E" into "M. E"
386
+ string = string.gsub(/\.(?=[[:alpha:]])/, '. ')
387
+ string = string.tr('-', ' ') if initialize_without_hyphen?
388
+
389
+ string.scan(/([^\s-]+)\s*(-)?\s*/).map { |part, hyphen|
390
+ part = initial_of(part) || " #{part} "
391
+ hyphen ? "#{part.rstrip}-" : part
392
+ }.join.gsub(/-\s+/, '-').squeeze(' ').strip
393
+ end
394
+
395
+ # @return [String, nil] the initial for the given part of a name
396
+ # or nil if the part should not be initialized
397
+ def initial_of(part)
398
+ case part
399
+ when /\A([[:upper:]])\z/, /\A(.+)\.\z/
400
+ "#{$1}#{initialize_with}"
401
+ when /\A[[:upper:]]{2}[[:lower:]]/
402
+ # Two letter initials like "Ts" for "TSerendorjiin"
403
+ "#{part[0, 2].capitalize}#{initialize_with}" unless initialize_existing_only?
404
+ when /\A[[:upper:]]/
405
+ "#{part[0]}#{initialize_with}" unless initialize_existing_only?
402
406
  end
403
407
  end
404
-
405
- def existing_initials_of(string)
406
- return unless string
407
- string = string.dup
408
-
409
- string.gsub!(/([[:upper:]])([[:upper:]])/, '\1 \2')
410
- string.gsub!(/\b([[:upper:]])\b[^[:alpha:]-]*/, "\\1#{initialize_with}")
411
-
412
- initialize_hyphen!(string)
413
-
414
- string.strip!
415
- string
416
- end
417
408
  end
418
409
 
419
410
 
@@ -13,9 +13,15 @@ module CiteProc
13
13
  ['i', 1]
14
14
  ].freeze
15
15
 
16
+ # Numbers (with optional prefixes and suffixes) or roman numerals
17
+ RANGE_BOUND = /(?:[[:alnum:]]*\d[[:alnum:]]*|[ivxlcdm]+)/i
18
+
19
+ # Ranges between numbers; escaped hyphens are not range delimiters
20
+ RANGE = /(?<![[:alnum:]])#{RANGE_BOUND}\s*[–-]\s*#{RANGE_BOUND}(?![[:alnum:]])/
21
+
16
22
  class << self
17
23
  def pluralize?(string)
18
- /\S[\s,&–-]\S|\df/ === string
24
+ /\S[\s,&]\S|\df/.match?(string) || RANGE.match?(string)
19
25
  end
20
26
 
21
27
  # @param number [#to_i] the number to convert
@@ -19,11 +19,12 @@ module CiteProc
19
19
 
20
20
  attr_reader :type, :conditions, :skip_conditions, :custom_matcher
21
21
 
22
- def initialize(attributes = nil)
22
+ def initialize(attributes = nil, &block)
23
23
  @conditions, @skip_conditions = {}, {}
24
24
 
25
25
  if block_given?
26
26
  @type = :ruby
27
+ @custom_matcher = block
27
28
 
28
29
  else
29
30
  unless attributes.nil? || attributes.empty?
@@ -54,12 +55,12 @@ module CiteProc
54
55
  @type = other.type
55
56
  @conditions = other.conditions.deep_copy
56
57
  @skip_conditions = other.skip_conditions.deep_copy
57
- @custom_matcher = other
58
+ @custom_matcher = other.custom_matcher
58
59
  end
59
60
 
60
61
  def type=(type)
61
62
  raise TypeError, "failed to set selector type to #{type.inspect}" unless
62
- type.respond_to(:to_sym) && Selector.types.include?(type.to_sym)
63
+ type.respond_to?(:to_sym) && Selector.types.include?(type.to_sym)
63
64
 
64
65
  @type = type.to_sym
65
66
  end
@@ -69,7 +70,7 @@ module CiteProc
69
70
  end
70
71
 
71
72
  def custom_matcher?
72
- defined?(@custom_matcher)
73
+ !custom_matcher.nil?
73
74
  end
74
75
 
75
76
  alias ruby? custom_matcher?
@@ -15,6 +15,17 @@ module CiteProc
15
15
  process(:bibliography, items, options)
16
16
  end
17
17
 
18
+ # @param value [String, Boolean, nil] an xsd:boolean value
19
+ # @param default [Boolean] the value if the value is not set
20
+ # @return [Boolean] the boolean value
21
+ def boolean(value, default = false)
22
+ case value.to_s
23
+ when 'true', '1' then true
24
+ when 'false', '0' then false
25
+ else default
26
+ end
27
+ end
28
+
18
29
  end
19
30
 
20
31
  end
@@ -131,12 +131,15 @@ module CiteProc
131
131
  # @return [Object] the value wrapped by this variable
132
132
  attr_accessor :value
133
133
 
134
+ # In-place transformations of the value
134
135
  def_delegators :@value, :to_s,
135
- *::String.instance_methods(false).select {|m| m.to_s =~ /!$/ }
136
+ :gsub!, :sub!, :upcase!, :downcase!, :capitalize!, :strip!
136
137
 
137
- def_delegators :to_s, :=~, :===, *String.instance_methods(false).reject { |m|
138
- m.to_s =~ /^[\W_]|[!=_]$|^(to_s|inspect|replace|first|last|dup|clone|to_f|to_i)$/
139
- }
138
+ # Variables quack sorta like a String
139
+ def_delegators :to_s,
140
+ :to_str, :empty?,
141
+ :=~, :===, :match, :match?, :start_with?, :end_with?, :include?,
142
+ :upcase, :downcase, :capitalize, :sub, :gsub, :split, :strip
140
143
 
141
144
 
142
145
  # Creates new Variable for the passed-in value
@@ -226,7 +229,7 @@ module CiteProc
226
229
  #
227
230
  # @return [Boolean] whether or not the variable's value is numeric
228
231
  def numeric?
229
- !!match(/^[\w\.:;]*\d+[\w\.:;]*(\s*[,&-]\s*[\w\.:;]*\d+[\w\.:;]*)*$/i)
232
+ !!to_s.match(/^[\w\.:;]*\d+[\w\.:;]*(\s*[,&-]\s*[\w\.:;]*\d+[\w\.:;]*)*$/i)
230
233
  end
231
234
 
232
235
  def date?
@@ -247,7 +250,7 @@ module CiteProc
247
250
 
248
251
  # @return [String] the variable's value stripped of markup
249
252
  def strip_markup
250
- gsub(Variable.markup, '')
253
+ to_s.gsub(Variable.markup, '')
251
254
  end
252
255
 
253
256
  # Strips markup off the variable's value.
@@ -274,6 +277,20 @@ module CiteProc
274
277
  end
275
278
  end
276
279
 
280
+ # Variables are equal (in the sense of eql?) if they are of
281
+ # the same type and have the same value.
282
+ #
283
+ # @param other [Object] the object used for comparison
284
+ # @return [Boolean]
285
+ def eql?(other)
286
+ other.instance_of?(self.class) && value.eql?(other.value)
287
+ end
288
+
289
+ # @return [Integer] a hash value based on the type and value
290
+ def hash
291
+ [self.class, value].hash
292
+ end
293
+
277
294
  # @!method to_s
278
295
  # @return [String] the variable's value as a string
279
296
  alias to_citeproc to_s
@@ -1,3 +1,3 @@
1
1
  module CiteProc
2
- VERSION = '1.2.0'.freeze
2
+ VERSION = '1.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: citeproc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvester Keil
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-05 00:00:00.000000000 Z
10
+ date: 2026-10-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: namae