citeproc 1.2.0 → 2.0.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: 21838f9d914cb947a14c36bf1b65271170de7a12d40eb069921e3a2ab3fe8a2a
4
+ data.tar.gz: bf916ee190ab7850caacde982b7510ffa045d2d5e276b30e9cd3a112ef644e7b
5
5
  SHA512:
6
- metadata.gz: b2e50088fd201c2546614cb5e90e6862d4b4b07ec3b5a87262b9bd70cfd453e1dc051ffaaa02379e10b709da5f5c455eca61b003b31049165a9ebf8f79f7ba63
7
- data.tar.gz: 636492e1a454f7f6a3ade9c30c1346cd036df0067be69b3d4102cf0b590df0959aa6dab6a619bd17333512ab6723c4d3a6823bc66fe6b334c2e9a001e5fffcae
6
+ metadata.gz: 7e4d56b5e112d3a3b94b931f648a75947efce1613f33ce147991ff9d8e2497ba33c002a547f403c9977ff65f6375ce2ebf7edbbe958ec3ea5b8daebfc819ba4c
7
+ data.tar.gz: 3be138495514a7f85ab8df3e22215c181069d0417aa8a863173592618fab37741e5b5691a3b52770deefcd7fb2bfed95077172c3f994b558e54daa0df11605db
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
  ----------------------
@@ -111,6 +110,18 @@ to install all official CSL styles and locales.
111
110
  To make the styles and locales available,
112
111
  simply `require 'csl/styles'`.
113
112
 
113
+ Development
114
+ -----------
115
+ To get started, install the development dependencies and run all tests:
116
+
117
+ $ bundle install
118
+ $ bundle exec rake
119
+
120
+ The [CSL test-suite](https://github.com/citation-style-language/test-suite)
121
+ runs as Cucumber features using CiteProc-Ruby:
122
+
123
+ $ bundle exec cucumber
124
+
114
125
  Credits
115
126
  -------
116
127
  Thanks to Rintze M. Zelle, Sebastian Karcher, Frank G. Bennett, Jr.,
@@ -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,46 @@ 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
+ # Only the locator up to the first embedded label (e.g., "fol."
136
+ # in "1, fol. 186") determines whether or not it is plural.
137
+ #
138
+ # @param abbreviations [Hash<String,String>] label abbreviations
139
+ # @return [Boolean] whether or not the locator is plural
140
+ def plural_locator?(abbreviations = CitationItem.locator_abbreviations)
141
+ pattern = CitationItem.locator_label_pattern(abbreviations)
142
+ Number.pluralize?(locator.to_s.split(/\s(?:#{pattern})\s/, 2)[0])
143
+ end
144
+
145
+ # Removes whitespace around the locator and moves a label at the
146
+ # start of the locator into the label (e.g., "vol. 1" becomes "1"
147
+ # with the label "volume") unless the item has a label other than
148
+ # "page".
149
+ #
150
+ # @param abbreviations [Hash<String,String>] label abbreviations
151
+ # @return [self]
152
+ def parse_locator!(abbreviations = CitationItem.locator_abbreviations)
153
+ return self unless locator?
154
+
155
+ self.locator = locator.to_s.strip
156
+ return self unless label.to_s == 'page'
157
+
158
+ pattern = CitationItem.locator_label_pattern(abbreviations)
159
+ match = /\A(#{pattern})\s+(.+)\z/m.match(locator.to_s)
160
+
161
+ unless match.nil?
162
+ self.label = abbreviations[match[1]]
163
+ self.locator = match[2]
164
+ end
165
+
166
+ self
167
+ end
168
+
69
169
  def initialize_copy(other)
70
170
  @attributes = other.attributes.deep_copy
71
171
  end
@@ -83,8 +183,6 @@ module CiteProc
83
183
  end
84
184
 
85
185
 
86
-
87
-
88
186
  class CitationData
89
187
 
90
188
  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
  #
@@ -80,7 +77,7 @@ module CiteProc
80
77
  d = arguments[0]
81
78
  super(d.year, d.month, d.day)
82
79
  else
83
- super(*arguments.map(&:to_i))
80
+ super(*arguments.map { |value| to_part(value) })
84
81
  end
85
82
  end
86
83
 
@@ -98,12 +95,18 @@ module CiteProc
98
95
  end
99
96
 
100
97
  parts.each_pair do |part, value|
101
- self[part] = value.nil? ? nil : value.to_i
98
+ self[part] = to_part(value)
102
99
  end
103
100
 
104
101
  self
105
102
  end
106
103
 
104
+ # @return [Integer, nil] the value as a date part; empty values are unset
105
+ def to_part(value)
106
+ value.to_i unless value.to_s.strip.empty?
107
+ end
108
+ private :to_part
109
+
107
110
  # @return [Boolean] whether or not the date parts are unset
108
111
  def empty?
109
112
  to_citeproc.empty?
@@ -129,6 +132,19 @@ module CiteProc
129
132
  !bc? && year < 1000
130
133
  end
131
134
 
135
+ # Seasons may be encoded as months 21 to 24 (Spring to Winter)
136
+ # and, as citeproc-js does, 13 to 20 (Spring to Winter, twice).
137
+ # @return [Integer, nil] the season (1 to 4) or nil if the
138
+ # month does not encode a season
139
+ def season
140
+ (month - 13) % 4 + 1 if month && month.between?(13, 24)
141
+ end
142
+
143
+ # @return [Boolean] whether or not the month encodes a season
144
+ def season?
145
+ !season.nil?
146
+ end
147
+
132
148
  # Formats the date parts according to the passed-in format string.
133
149
  # @param format [String] a format string
134
150
  # @return [String,nil] the formatted date string; nil if the date
@@ -148,8 +164,10 @@ module CiteProc
148
164
  # @return [Fixnum,nil] the result of the comparison (-1, 0, 1 or nil)
149
165
  def <=>(other)
150
166
  case
167
+ when other.is_a?(DateParts) && season? && other.season?
168
+ [year, season] <=> [other.year, other.season]
151
169
  when other.is_a?(DateParts)
152
- to_citeproc <=> other.to_citeproc
170
+ comparable <=> other.comparable
153
171
  when other.respond_to?(:to_date)
154
172
  to_date <=> other.to_date
155
173
  else
@@ -192,6 +210,14 @@ module CiteProc
192
210
  def inspect
193
211
  "#<DateParts #{to_s}>"
194
212
  end
213
+
214
+ protected
215
+
216
+ # Seasons are compared like dates without month.
217
+ # @return [Array<Fixnum>] the date parts used for comparison
218
+ def comparable
219
+ season? ? to_citeproc.take(1) : to_citeproc
220
+ end
195
221
  end
196
222
 
197
223
  #
@@ -210,13 +236,11 @@ module CiteProc
210
236
  # List of date parsers (must respond to #parse)
211
237
  @parsers = []
212
238
 
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
239
+ begin
240
+ require 'edtf'
241
+ @parsers << ::EDTF
242
+ rescue LoadError
243
+ # warn "failed to load `edtf' gem"
220
244
  end
221
245
 
222
246
  @parsers << ::Date
@@ -229,9 +253,8 @@ module CiteProc
229
253
  # A list of available date parsers. Each parser must respond to a
230
254
  # #parse method that converts a date string into a Ruby date object.
231
255
  # 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`.
256
+ # standard library, as well as the parser of the EDTF gem if it is
257
+ # available; to install it on your system run `gem install edtf`.
235
258
  #
236
259
  # @return [Array] the available date parsers
237
260
  attr_reader :parsers
@@ -276,12 +299,44 @@ module CiteProc
276
299
  end
277
300
 
278
301
 
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
- }
302
+ def_delegators :to_ruby, :strftime
303
+
304
+ attr_predicates :circa, :literal, :'date-parts'
305
+
306
+ # @return [Boolean] whether or not the date has a season field
307
+ def has_season?
308
+ attribute?(:season)
309
+ end
283
310
 
284
- attr_predicates :circa, :season, :literal, :'date-parts'
311
+ def season=(season)
312
+ write_attribute :season, season
313
+ end
314
+
315
+ # Season names recognized in the season field
316
+ SEASONS = {
317
+ 'spring' => 1, 'summer' => 2, 'autumn' => 3, 'fall' => 3, 'winter' => 4
318
+ }.freeze
319
+
320
+ # A season takes the place of the month. If the (start) date has a
321
+ # month, the season is encoded by the month (21 to 24); otherwise
322
+ # the season field is used. Season numbers (1 to 4) and names in
323
+ # the season field are converted to integers.
324
+ #
325
+ # @return [Integer, String, nil] the season (1 to 4), the season
326
+ # field as a string if it is not a season number or name, or nil
327
+ def season
328
+ return parts[0].season if month
329
+
330
+ value = read_attribute(:season) or return
331
+ name = value.to_s.strip.downcase
332
+
333
+ SEASONS[name] || (name.match?(/\A[1-4]\z/) ? name.to_i : value.to_s)
334
+ end
335
+
336
+ # @return [Boolean] whether or not the date has a season (see #season)
337
+ def season?
338
+ !season.nil?
339
+ end
285
340
 
286
341
  def initialize(value = {})
287
342
  super
@@ -321,11 +376,15 @@ module CiteProc
321
376
  when value.is_a?(Hash)
322
377
  attributes = value.symbolize_keys
323
378
 
324
- if attributes.has_key?(:raw)
325
- @value = Date.parse(attributes.delete(:raw)).value
379
+ if attributes.has_key?(:raw) && Array(attributes[:'date-parts']).empty?
380
+ raw = attributes.delete(:raw)
381
+ attributes.delete(:'date-parts')
382
+ date = Date.parse(raw)
383
+
384
+ @value = date.nil? ? { :literal => raw } : date.value
326
385
  @value.merge!(attributes)
327
386
  else
328
- @value = attributes.deep_copy
387
+ @value = attributes.except(:raw).deep_copy
329
388
  end
330
389
  convert_parts!
331
390
 
@@ -397,34 +456,16 @@ module CiteProc
397
456
  d
398
457
  end
399
458
 
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
459
+ # @return [::Date, Range, nil] the date as a Ruby date object or as
460
+ # a Range if this instance is closed range
415
461
  def to_ruby
416
462
  if closed_range?
417
- start_date..end_date
463
+ parts[0].to_date..parts[1].to_date
418
464
  else
419
- start_date
465
+ d = parts[0] and d.to_date
420
466
  end
421
467
  end
422
468
 
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
469
  # @return [Boolean] whether or not the date-parts contain an end date
429
470
  def has_end_date?
430
471
  parts[1] && !parts[1].empty?
@@ -506,7 +547,7 @@ module CiteProc
506
547
  case
507
548
  when literal?
508
549
  literal
509
- when season?
550
+ when has_season?
510
551
  season
511
552
  else
512
553
  parts.map(&:to_citeproc).inspect
@@ -516,8 +557,14 @@ module CiteProc
516
557
  def <=>(other)
517
558
  case other
518
559
  when CiteProc::Date
519
- return nil if season? || other.season?
520
- parts <=> other.parts
560
+ result = parts <=> other.parts
561
+
562
+ # Seasons are ordered among themselves only
563
+ if result == 0 && season? && other.season?
564
+ result = season.to_i <=> other.season.to_i
565
+ end
566
+
567
+ result
521
568
  when ::Date
522
569
  parts <=> [other]
523
570
  else
data/lib/citeproc/item.rb CHANGED
@@ -74,11 +74,28 @@ module CiteProc
74
74
  protected :attributes
75
75
 
76
76
 
77
+ # Legacy names of variables in CSL-JSON
78
+ LEGACY_NAMES = {
79
+ :journalAbbreviation => :'container-title-short',
80
+ :shortTitle => :'title-short'
81
+ }.freeze
82
+
77
83
  def initialize(attributes = nil)
78
84
  merge(attributes)
79
85
  yield self if block_given?
80
86
  end
81
87
 
88
+ def merge(other)
89
+ super
90
+
91
+ LEGACY_NAMES.each do |legacy, name|
92
+ value = attributes.delete(legacy)
93
+ write_attribute(name, value) unless value.nil? || attribute?(name)
94
+ end
95
+
96
+ self
97
+ end
98
+
82
99
  def initialize_copy(other)
83
100
  @attributes = other.attributes.deep_copy
84
101
  end
@@ -100,14 +117,24 @@ module CiteProc
100
117
  end
101
118
 
102
119
  def observable_read_attribute(key)
103
- value = original_read_attribute(key)
104
- return if suppressed?(key)
105
- value
120
+ value = original_read_attribute(key) unless suppressed?(key)
106
121
  ensure
107
122
  changed
108
123
  notify_observers :read, key, value
109
124
  end
110
125
 
126
+ # Reads the variable without notifying observers and yields it;
127
+ # observers are notified of the block's result instead
128
+ # (e.g., what a renderer made of the variable).
129
+ # @return the block's result
130
+ def deferred_read_attribute(key)
131
+ value = original_read_attribute(key) unless suppressed?(key)
132
+ result = yield value
133
+ ensure
134
+ changed
135
+ notify_observers :read, key, result
136
+ end
137
+
111
138
  def simulate_read_attribute(key, value)
112
139
  changed
113
140
  notify_observers :read, key, value
@@ -42,6 +42,15 @@ module CiteProc
42
42
  include Attributes
43
43
  include Comparable
44
44
 
45
+ # Leading particle of a family name: "van der Vlist", "d'Alembert", "al-Hassan"
46
+ FAMILY_PARTICLE = /\A(?<particle>\S+[-ʻ’' ] *)(?<name>.+)\z/
47
+
48
+ # Trailing particle of a given name: "Alexander von"
49
+ GIVEN_PARTICLE = /\A(?<name>.+)\s+(?<particle>\S+)\z/
50
+
51
+ # Suffix after a comma in a given name: "John, III" or "John,! Jr."
52
+ SUFFIX = /\A(?<given>.*?)\s*,(?<comma>!?)\s*(?<suffix>.+)\z/
53
+
45
54
  # Class instance variables
46
55
 
47
56
  @romanesque =
@@ -51,7 +60,7 @@ module CiteProc
51
60
  @defaults = {
52
61
  :form => 'long',
53
62
  :'name-as-sort-order' => false,
54
- :'demote-non-dropping-particle' => :never,
63
+ :'demote-non-dropping-particle' => 'display-and-sort',
55
64
  :'sort-separator' => ', ',
56
65
  :initialize => true,
57
66
  :'initialize-with-hyphen' => true,
@@ -92,20 +101,19 @@ module CiteProc
92
101
  end
93
102
 
94
103
  # 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
- }
104
+ def_delegators :to_s,
105
+ :to_str,
106
+ :=~, :===, :match, :match?, :start_with?, :end_with?, :include?,
107
+ :upcase, :downcase, :capitalize, :sub, :gsub, :split, :strip
98
108
 
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
109
+ # Delegate in-place transformations to each field's value
110
+ [:gsub!, :sub!, :upcase!, :downcase!, :capitalize!, :strip!].each do |m|
111
+ define_method(m) do |*arguments, &block|
112
+ Name.parts.each do |part|
113
+ p = attributes[part]
114
+ p.send(m, *arguments, &block) if p.respond_to?(m)
108
115
  end
116
+ self
109
117
  end
110
118
  end
111
119
 
@@ -127,6 +135,30 @@ module CiteProc
127
135
  end
128
136
 
129
137
 
138
+ # Parses name particles and suffixes out of the family and given names
139
+ # and sets parse-names to false, so the name is not parsed again.
140
+ # Names with parse-names set to false are not parsed.
141
+ # Quotes around the family name are removed
142
+ # and the family name is not parsed.
143
+ # Names without family or given name, or with any particle or suffix set,
144
+ # are not parsed.
145
+ #
146
+ # @return [self]
147
+ def parse!
148
+ return self unless CiteProc.boolean(read_attribute(:'parse-names'), true)
149
+
150
+ quoted = family? && family.match?(/\A".+"\z/)
151
+ self.family = family[1...-1] if quoted
152
+
153
+ if family? && given? && !(particle? || dropping_particle? || suffix?)
154
+ parse_family! unless quoted
155
+ parse_given!
156
+ end
157
+
158
+ write_attribute(:'parse-names', false)
159
+ self
160
+ end
161
+
130
162
  # Resets the object's options to the default settings.
131
163
  # @return [self]
132
164
  def reset!
@@ -167,6 +199,12 @@ module CiteProc
167
199
  static_ordering? || !romanesque?
168
200
  end
169
201
 
202
+ # @return [Boolean] whether or not the name can be printed in sort
203
+ # order (literal names and names in static order cannot)
204
+ def invertible?
205
+ personal? && !static_order?
206
+ end
207
+
170
208
  # Set the name to use static order for printing, i.e., print the family
171
209
  # name before the given name as is customary, for example, in Hungarian
172
210
  # and many Asian languages.
@@ -236,7 +274,7 @@ module CiteProc
236
274
 
237
275
  # @return [Boolean] whether or not initials will be used for printing
238
276
  def initials?
239
- !!options[:'initialize-with'] && personal? && romanesque?
277
+ !!options[:'initialize-with'] && personal? && family? && romanesque?
240
278
  end
241
279
 
242
280
  def initialize_with
@@ -244,7 +282,7 @@ module CiteProc
244
282
  end
245
283
 
246
284
  def initialize_existing_only?
247
- options[:initialize].to_s == 'false'
285
+ !CiteProc.boolean(options[:initialize], true)
248
286
  end
249
287
 
250
288
  def initialize_without_hyphen?
@@ -259,16 +297,15 @@ module CiteProc
259
297
  case
260
298
  when !initials?
261
299
  given
262
- when initialize_existing_only?
263
- existing_initials_of given
264
300
  else
265
301
  initials_of given
266
302
  end
267
303
  end
268
304
 
305
+ # @return [Boolean] whether or not the non-dropping particle is demoted
306
+ # when the name is printed; "sort-only" demotes it only in sort keys
269
307
  def demote_non_dropping_particle?
270
- always_demote_non_dropping_particle? ||
271
- !!(sort_order? && options[:'demote-non-dropping-particle'] =~ /^sort(-only)?$/i)
308
+ always_demote_non_dropping_particle?
272
309
  end
273
310
 
274
311
  alias demote_particle? demote_non_dropping_particle?
@@ -316,30 +353,66 @@ module CiteProc
316
353
  end
317
354
 
318
355
  # @return [String] the name formatted according to the current options
319
- def format
320
- case
321
- when literal?
322
- literal.to_s
323
- when static_order?
324
- [family, initials].compact.join(' ')
325
- when !short_form?
326
- case
327
- when !sort_order?
328
- [[initials, dropping_particle, particle, family].compact_join(' '),
329
- suffix].compact_join(comma_suffix? ? comma : ' ')
356
+ # Formats the name according to the formatting options. The name
357
+ # parts may be formatted and enclosed in affixes: the formatter is
358
+ # called with the name part (:given or :family) and its text, and the
359
+ # affixer with the name part and the text it encloses. The given
360
+ # formatting applies to the given name and the dropping particle, the
361
+ # family formatting to the family name and the non-dropping particle.
362
+ # The family affixes enclose all preceding particles and, for names in
363
+ # display order, the suffix; the given affixes enclose the particles
364
+ # following the given name in sort order.
365
+ #
366
+ # @param formatter [#call] formats the text of a name part
367
+ # @param affixer [#call] encloses the text of a name part in affixes
368
+ # @return [String] the formatted name
369
+ def format(formatter = nil, affixer = nil)
370
+ format_part = ->(part, text) { formatter.nil? || text.to_s.empty? ? text : formatter.(part, text) }
371
+ affix_part = ->(part, text) { affixer.nil? || text.to_s.empty? ? text : affixer.(part, text) }
330
372
 
331
- when !demote_particle?
332
- [[particle, family].compact_join(' '), [initials,
333
- dropping_particle].compact_join(' '), suffix].compact_join(comma)
373
+ return affix_part.(:family, format_part.(:family, literal.to_s)) if literal?
334
374
 
335
- else
336
- [family, [initials, dropping_particle, particle].compact_join(' '),
337
- suffix].compact_join(comma)
338
- end
375
+ given = format_part.(:given, initials)
376
+ dropping = format_part.(:given, dropping_particle)
377
+ particle = format_part.(:family, self.particle)
378
+ family = format_part.(:family, self.family)
379
+ particle_family = [particle, family].compact_join(particle_separator)
380
+
381
+ case
382
+ when static_order?
383
+ [affix_part.(:family, family), affix_part.(:given, given)]
384
+ .compact_join(romanesque? ? ' ' : '')
385
+ when short_form?
386
+ affix_part.(:family, particle_family)
387
+ when !sort_order?
388
+ given = affix_part.(:given, given)
389
+ family = [[dropping, particle_family].compact_join(particle_separator(dropping_particle)),
390
+ suffix].compact_join(comma_suffix? ? comma : ' ')
391
+
392
+ # Affixes ending in a space (e.g., a no-break space) replace the space
393
+ [given, affix_part.(:family, family)]
394
+ .compact_join(given.to_s.match?(/[[:space:]]\z/) ? '' : ' ')
395
+ when !demote_particle?
396
+ [affix_part.(:family, particle_family),
397
+ affix_part.(:given, [given, dropping].compact_join(' ')), suffix].compact_join(comma)
339
398
  else
340
- [particle, family].compact_join(' ')
399
+ [affix_part.(:family, family),
400
+ affix_part.(:given, [given, dropping, particle].compact_join(' ')), suffix].compact_join(comma)
341
401
  end
342
402
  end
403
+
404
+ # @return [String] the family name preceded by the non-dropping particle
405
+ def particle_family
406
+ [particle, family].compact_join(particle_separator)
407
+ end
408
+
409
+ # @param particle [String] the dropping or non-dropping particle
410
+ # @return [String] the separator following the particle; particles
411
+ # ending in a hyphen or apostrophe (e.g., "al-" or "d'") are joined
412
+ # without a space
413
+ def particle_separator(particle = self.particle)
414
+ particle.to_s.match?(/[-'’ʻ\s]\z/) ? '' : ' '
415
+ end
343
416
  alias print format
344
417
 
345
418
  # @return [Array<String>] an ordered array of formatted name parts to be used for sorting
@@ -348,15 +421,15 @@ module CiteProc
348
421
  when literal?
349
422
  [literal.to_s.sub(sort_prefix, '')]
350
423
  when never_demote_particle?
351
- [[particle, family].compact_join(' '), dropping_particle, given, suffix].map(&:to_s)
424
+ [particle_family, dropping_particle, given, suffix].map(&:to_s)
352
425
  else
353
- [family, [particle, dropping_particle].compact_join(' '), given, suffix].map(&:to_s)
426
+ [family, [dropping_particle, particle].compact_join(' '), given, suffix].map(&:to_s)
354
427
  end
355
428
  end
356
429
 
357
430
  # @return [String] the name as a string stripped off all markup
358
431
  def strip_markup
359
- gsub(Variable.markup, '')
432
+ to_s.gsub(Variable.markup, '')
360
433
  end
361
434
 
362
435
  # @return [self] the name with all parts stripped off markup
@@ -382,37 +455,72 @@ module CiteProc
382
455
  super key
383
456
  end
384
457
 
385
- def initials_of(string)
386
- return unless string
387
- string = string.dup
458
+ # Leading lowercase words of the family name become the non-dropping particle;
459
+ # particles may be joined by hyphens or apostrophes ("d'Alembert", "al-Hassan").
460
+ def parse_family!
461
+ particles, name = '', family.to_s
462
+ while (m = FAMILY_PARTICLE.match(name)) && particle_word?(m[:particle])
463
+ particles, name = particles + m[:particle], m[:name]
464
+ end
388
465
 
389
- string.gsub!(/([[:upper:]])[^[:upper:]\s-]*\s*/, "\\1#{initialize_with}")
466
+ unless particles.empty?
467
+ self.family = name
468
+ self.particle = particles.match?(/['’] \z/) ? particles.rstrip + ' ' : particles.rstrip
469
+ end
470
+ end
390
471
 
391
- initialize_hyphen!(string)
472
+ # Text after a comma in the given name becomes the suffix
473
+ # ("John, III"; with "John,! Jr." the suffix keeps its comma)
474
+ # and trailing lowercase words the dropping particle.
475
+ def parse_given!
476
+ if (m = SUFFIX.match(given.to_s))
477
+ self.given, self.suffix = m[:given], m[:suffix]
478
+ self.comma_suffix = true unless m[:comma].empty?
479
+ end
392
480
 
393
- string.strip!
394
- string
395
- end
481
+ particles, name = [], given.to_s
482
+ while (m = GIVEN_PARTICLE.match(name)) && particle_word?(m[:particle])
483
+ particles.unshift(m[:particle])
484
+ name = m[:name]
485
+ end
396
486
 
397
- def initialize_hyphen!(string)
398
- if initialize_without_hyphen?
399
- string.tr!('-', '')
400
- else
401
- string.gsub!(/\s*-/, '-')
487
+ unless particles.empty?
488
+ self.given = name
489
+ self.dropping_particle = particles.join(' ')
402
490
  end
403
491
  end
404
492
 
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}")
493
+ def particle_word?(word)
494
+ word.sub(/\A[-'ʻ’\s]*/, '').match?(/\A\p{Ll}/)
495
+ end
411
496
 
412
- initialize_hyphen!(string)
497
+ def initials_of(string)
498
+ return unless string
413
499
 
414
- string.strip!
415
- string
500
+ # Split abbreviations like "M.E" into "M. E"
501
+ string = string.gsub(/\.(?=[[:alpha:]])/, '. ')
502
+ string = string.tr('-', ' ') if initialize_without_hyphen?
503
+
504
+ # Hyphens followed by a lowercase letter (e.g., "Guo-ping") do not
505
+ # separate parts of the name
506
+ string.scan(/((?:[^\s-]|-(?=\p{Ll}))+)\s*(-)?\s*/).map { |part, hyphen|
507
+ part = initial_of(part) || " #{part} "
508
+ hyphen ? "#{part.rstrip}-" : part
509
+ }.join.gsub(/-\s+/, '-').squeeze(' ').strip
510
+ end
511
+
512
+ # @return [String, nil] the initial for the given part of a name
513
+ # or nil if the part should not be initialized
514
+ def initial_of(part)
515
+ case part
516
+ when /\A([[:upper:]])\z/, /\A(.+)\.\z/
517
+ "#{$1}#{initialize_with}"
518
+ when /\A[[:upper:]]{2}[[:lower:]]/
519
+ # Two letter initials like "Ts" for "TSerendorjiin"
520
+ "#{part[0, 2].capitalize}#{initialize_with}" unless initialize_existing_only?
521
+ when /\A[[:upper:]]/
522
+ "#{part[0]}#{initialize_with}" unless initialize_existing_only?
523
+ end
416
524
  end
417
525
  end
418
526
 
@@ -566,7 +674,7 @@ module CiteProc
566
674
  when value.is_a?(Name)
567
675
  @value << value
568
676
  when value.respond_to?(:each_pair), value.respond_to?(:to_hash)
569
- @value << Name.new(value)
677
+ @value << Name.new(value).parse!
570
678
  when value.respond_to?(:to_s)
571
679
  begin
572
680
  @value.concat Namae.parse!(value.to_s).map { |n| Name.new n }
@@ -13,9 +13,22 @@ 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
+
22
+ PAGE_RANGE = /([[:alnum:]]+)\s*[–-]+\s*([[:alnum:]]+)/
23
+
24
+ # A page number with optional prefix and suffix
25
+ PAGE = /\A(\d*[[:alpha:]]+)?(\d+)([[:alpha:]]*)\z/
26
+
27
+ ROMAN = /\A[ivxlcdm]+\z/i
28
+
16
29
  class << self
17
30
  def pluralize?(string)
18
- /\S[\s,&–-]\S|\df/ === string
31
+ /\S[\s,&]\S|\df/.match?(string) || RANGE.match?(string)
19
32
  end
20
33
 
21
34
  # @param number [#to_i] the number to convert
@@ -31,6 +44,108 @@ module CiteProc
31
44
  code * count
32
45
  }.join
33
46
  end
47
+
48
+ # Formats the page ranges in pages according to format:
49
+ #
50
+ # * "chicago-15" (or "chicago") and "chicago-16": page ranges are
51
+ # abbreviated according to the Chicago Manual of Style rules.
52
+ # * "expanded": Abbreviated page ranges are expanded to
53
+ # their non-abbreviated form: 42-45, 321-328, 2787-2816.
54
+ # * "minimal": All digits repeated in the second number
55
+ # are left out: 42-45, 321-8, 2787-816.
56
+ # * "minimal-two": As "minimal", but at least two digits
57
+ # are kept in the second number: 42-45, 321-28, 2787-816.
58
+ #
59
+ # Without a format only the range delimiter is replaced. Ranges
60
+ # with different prefixes (e.g., "N110-5") keep a plain hyphen;
61
+ # escaped hyphens ("\-") are not range delimiters.
62
+ #
63
+ # @param pages [String] the pages to format
64
+ # @param format [String, nil] the page range format
65
+ # @param delimiter [String] the range delimiter
66
+ # @return [String, nil] the formatted pages
67
+ def format_page_range(pages, format = nil, delimiter = '–')
68
+ return if pages.nil?
69
+
70
+ pages.to_s
71
+ .gsub(PAGE_RANGE) { format_page_bounds($1, $2, format, delimiter) || "#{$1}-#{$2}" }
72
+ .gsub('\\-', '-')
73
+ end
74
+
75
+ private
76
+
77
+ # @return [String, nil] the formatted page range or nil if
78
+ # the bounds do not form a page range
79
+ def format_page_bounds(from, to, format, delimiter)
80
+ if ROMAN.match?(from) && ROMAN.match?(to)
81
+ return "#{from}#{delimiter}#{to}"
82
+ end
83
+
84
+ f, t = PAGE.match(from), PAGE.match(to)
85
+
86
+ # Ranges must have the same prefix on both sides
87
+ return unless f && t && f[1] == t[1]
88
+
89
+ # When there are suffixes or no format was
90
+ # specified we only replace the delimiter
91
+ if format.nil? || !f[3].empty? || !t[3].empty?
92
+ return "#{from}#{delimiter}#{to}"
93
+ end
94
+
95
+ prefix = f[1]
96
+ last = format_page_number(f[2], t[2].dup, format)
97
+
98
+ # The prefix is repeated only for expanded ranges
99
+ "#{prefix}#{f[2]}#{delimiter}#{prefix if format == 'expanded'}#{last}"
100
+ end
101
+
102
+ def format_page_number(f, t, format)
103
+ dim = f.length
104
+ delta = dim - t.length
105
+
106
+ if delta >= 0
107
+ t.prepend f[0, delta] unless delta.zero?
108
+
109
+ format = 'chicago-15' if format == 'chicago'
110
+
111
+ if format == 'chicago-15' || format == 'chicago-16'
112
+ # Only the 15th edition expands four digit
113
+ # numbers when three or more digits change
114
+ changes = dim - f.chars.zip(t.chars).
115
+ take_while { |a,b| a == b }.length if dim == 4
116
+
117
+ format = case
118
+ when dim < 3
119
+ 'expanded'
120
+ when dim == 4 && format == 'chicago-15' && changes > 2
121
+ 'expanded'
122
+ when f[-2, 2] == '00'
123
+ 'expanded'
124
+ when f[-2] == '0'
125
+ 'minimal'
126
+ else
127
+ 'minimal-two'
128
+ end
129
+ end
130
+
131
+ case format
132
+ when 'expanded'
133
+ # nothing to do
134
+ when 'minimal'
135
+ t = t.each_char.drop_while.with_index { |c, i| c == f[i] }.join('')
136
+ when 'minimal-two'
137
+ if dim > 2
138
+ t = t.each_char.drop_while.with_index { |c, i|
139
+ c == f[i] && dim - i > 2
140
+ }.join('')
141
+ end
142
+ else
143
+ raise ArgumentError, "unknown page range format: #{format}"
144
+ end
145
+ end
146
+
147
+ t
148
+ end
34
149
  end
35
150
 
36
151
  def <=>(other)
@@ -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,26 @@ module CiteProc
15
15
  process(:bibliography, items, options)
16
16
  end
17
17
 
18
+ # @param string [String, nil]
19
+ # @return [Boolean] whether or not the string starts with a letter of
20
+ # a script which separates words by spaces (as decided by citeproc-js);
21
+ # the Hebrew conjunction "ו" is excluded because it is written as a
22
+ # prefix of the following word
23
+ def romanesque_start?(string)
24
+ /\A(?!ו)[&\p{Latin}\p{Greek}\p{Cyrillic}\p{Hebrew}\p{Arabic}\p{Thai}]/.match?(string.to_s)
25
+ end
26
+
27
+ # @param value [String, Boolean, nil] an xsd:boolean value
28
+ # @param default [Boolean] the value if the value is not set
29
+ # @return [Boolean] the boolean value
30
+ def boolean(value, default = false)
31
+ case value.to_s
32
+ when 'true', '1' then true
33
+ when 'false', '0' then false
34
+ else default
35
+ end
36
+ end
37
+
18
38
  end
19
39
 
20
40
  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 = '2.0.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: 2.0.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: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: namae
@@ -15,56 +15,56 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: '1.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: '1.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: date
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
32
+ version: '3.0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '3.0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: forwardable
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '1.3'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '1.3'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: json
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: '2.0'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
67
+ version: '2.0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: observer
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - "<"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '1.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: citeproc-ruby
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
96
110
  description: A cite processor interface for Citation Style Language (CSL) styles.
97
111
  email:
98
112
  - sylvester@keil.or.at
@@ -124,7 +138,10 @@ files:
124
138
  homepage: https://github.com/inukshuk/citeproc
125
139
  licenses:
126
140
  - BSD-2-Clause
127
- metadata: {}
141
+ metadata:
142
+ source_code_uri: https://github.com/inukshuk/citeproc
143
+ bug_tracker_uri: https://github.com/inukshuk/citeproc/issues
144
+ rubygems_mfa_required: 'true'
128
145
  rdoc_options: []
129
146
  require_paths:
130
147
  - lib