citeproc-ruby 2.2.0 → 2.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: 1caf4a2b5f09b04fd3e6b5a8e7fbaf4c1e8e5b45d4c3c7ce8856eb79990f74ee
4
- data.tar.gz: 38b1c2eeff222fb886cf2a95c146835fe72bdcb64aef5fd4bf8034c2113792d6
3
+ metadata.gz: 711627337c8ad301400254ed5e9143c43ac710f4aeeed58e2497384d067e7d88
4
+ data.tar.gz: b50e0a76599302904b67922a7799d1e88bc57e57994936a50753c45a54d74aab
5
5
  SHA512:
6
- metadata.gz: cc45e7b4b001ce4caa03c7b16a039ffc0e141e99ee30758ffe230201f4671c7a3b6da05b342a503583f4f6668231bb22cad4780c688417d1e57b04bcbbc2278d
7
- data.tar.gz: f2d8c8bc3919d4973335447ce04850902459ec625c236cdbdee02a9a285a9cbbbdd933ccc147edaf5032386900b39ed101680911411e8dd6a616e19a0d9bc748
6
+ metadata.gz: 2922fa4307ebe6a89970118a55be01322ee21fd47f7512f7b34656842c61e3ec409180bc25ae2555a48faf8c63a6d2e87aff1148085af52f011d1ce9ca2b723d
7
+ data.tar.gz: ff9ceb486663023a3c4fbe2d01eacb17208cb26743ba60aecf1fc707423d0c8a0a5a39e9e3f48e2e2f2fe5b4b21a759b81556ae6d0d4834b8ac3d4e574798bb4
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  CiteProc-Ruby
2
2
  =============
3
- CiteProc-Ruby is a [Citation Style Language](http://citationstyles.org/) (CSL)
4
- 1.0.2 cite processor written in pure Ruby. This Ruby gem contains the
5
- processor's rendering engine only; for more documentation on the whole
6
- cite processor, please refer to the documentation of the
3
+ CiteProc-Ruby is a [Citation Style Language](https://citationstyles.org/) (CSL)
4
+ 1.0.2 cite processor written in pure Ruby.
5
+ This Ruby gem contains the processor's rendering engine only;
6
+ for more documentation on the whole cite processor,
7
+ please refer to the documentation of the
7
8
  [citeproc](https://rubygems.org/gems/citeproc) gem instead.
8
9
 
9
10
  Quickstart
@@ -13,64 +14,65 @@ Install CiteProc-Ruby and all official CSL styles (optional).
13
14
  $ [sudo] gem install citeproc-ruby
14
15
  $ [sudo] gem install csl-styles
15
16
 
16
- Start rendering you references with any CSL style!
17
+ Start rendering your references with any CSL style!
17
18
 
18
19
  ```ruby
19
20
  require 'citeproc'
20
21
  require 'csl/styles'
22
+ require 'bibtex'
21
23
 
22
- # Create a new processor with the desired style,
23
- # format, and locale.
24
+ # Create a new processor
25
+ # with the desired style, format, and locale.
24
26
  cp = CiteProc::Processor.new style: 'apa', format: 'text'
25
27
 
26
- # To see what styles are available in your current
27
- # environment, run `CSL::Style.ls'; this also works for
28
- # locales as `CSL::Locale.ls'.
28
+ # To see what styles are available in your current environment,
29
+ # run `CSL::Style.ls';
30
+ # this also works for locales as `CSL::Locale.ls'.
29
31
 
30
- # Tell the processor where to find your references. In this
31
- # example we load them from a BibTeX bibliography using the
32
- # bibtex-ruby gem.
32
+ # Tell the processor where to find your references.
33
+ # In this example we load them from a BibTeX bibliography
34
+ # using the bibtex-ruby gem (`gem install bibtex-ruby').
33
35
  cp.import BibTeX.open('./references.bib').to_citeproc
34
36
 
35
- # Now you are ready for rendering; the processor API
36
- # provides three main rendering methods: `process',
37
- # `append', or `bibliography'.
37
+ # Now you are ready for rendering;
38
+ # the processor API provides two main rendering methods:
39
+ # `process' and `bibliography'.
38
40
 
39
- # For simple one-off renditions, you can also call
40
- # `render' in bibliography or citation mode:
41
+ # For simple one-off renditions,
42
+ # you can also call `render' in bibliography or citation mode:
41
43
  cp.render :bibliography, id: 'knuth'
42
44
 
43
45
  # This will return a rendered reference, like:
44
- #-> Knuth, D. (1968). The art of computer programming. Boston: Addison-Wesley.
45
-
46
- # CiteProc-Ruby exposes a full CSL API to you; this
47
- # makes it possible to just alter CSL styles on the
48
- # fly. For example, what if we want names not to be
49
- # initialized even though APA style is configured to
50
- # do so? We could change the CSL style itself, but
51
- # we can also make a quick adjustment at runtime:
52
- name = cp.engine.style.macros['author'] > 'names' > 'name'
53
-
54
- # What just happened? We selected the current style's
55
- # 'author' macro and then descended to the CSL name
56
- # node via its parent names node. Now we can change
57
- # this name node and the cite processor output will
58
- # pick-up the changes right away:
59
- name[:initialize] = 'false'
46
+ #-> Knuth, D. (1968). The art of computer programming (Vol. 1). Addison-Wesley.
47
+
48
+ # CiteProc-Ruby exposes a full CSL API to you;
49
+ # this makes it possible to just alter CSL styles on the fly.
50
+ # For example, what if we want names not to be initialized
51
+ # even though APA style is configured to do so?
52
+ # We could change the CSL style itself,
53
+ # but we can also make a quick adjustment at runtime:
54
+ cp.engine.style.macros['author-bib'].each_descendant do |node|
55
+ node[:initialize] = 'false' if node.nodename == 'name'
56
+ end
57
+
58
+ # What just happened?
59
+ # We selected the current style's 'author-bib' macro
60
+ # and changed all of its CSL name nodes;
61
+ # the cite processor output will pick-up the changes right away:
60
62
 
61
63
  cp.render :bibliography, id: 'knuth'
62
- #-> Knuth, Donald. (1968). The art of computer programming (Vol. 1). Boston: Addison-Wesley.
64
+ #-> Knuth, Donald. (1968). The art of computer programming (Vol. 1). Addison-Wesley.
63
65
 
64
66
  # Note that we have picked 'text' as the output format;
65
- # if we want to make us of richer output formats we
66
- # can switch to HTML instead:
67
+ # if we want to make use of richer output formats
68
+ # we can switch to HTML instead:
67
69
  cp.engine.format = 'html'
68
70
 
69
71
  cp.render :bibliography, id: 'knuth'
70
- #-> Knuth, Donald. (1968). <i>The art of computer programming</i> (Vol. 1). Boston: Addison-Wesley.
72
+ #-> Knuth, Donald. (1968). <i>The art of computer programming</i> (Vol. 1). Addison-Wesley.
71
73
 
72
74
  # You can also render citations on the fly.
73
- cp.render :citation, id: 'knuth', locator: '23'
75
+ cp.render :citation, id: 'knuth', locator: '23', label: 'page'
74
76
  #-> (Knuth, 1968, p. 23)
75
77
 
76
78
  # Pass an array if you want to render multiple citations:
@@ -80,38 +82,40 @@ cp.render :citation, [{ id: 'knuth' }, { id: 'perez' }]
80
82
 
81
83
  Documentation
82
84
  -------------
83
- * [CiteProc Documentation](http://rubydoc.info/gems/citeproc/)
84
- * [CiteProc-Ruby API Documentation](http://rubydoc.info/gems/citeproc-ruby/)
85
- * [CSL-Ruby API Documentation](http://rubydoc.info/gems/csl/)
85
+ * [CiteProc Documentation](https://rubydoc.info/gems/citeproc/)
86
+ * [CiteProc-Ruby API Documentation](https://rubydoc.info/gems/citeproc-ruby/)
87
+ * [CSL-Ruby API Documentation](https://rubydoc.info/gems/csl/)
86
88
 
87
89
  Optional Dependencies
88
90
  ---------------------
89
91
  CiteProc-Ruby tries to minimize hard dependencies for increased compatibility.
90
92
  You can speed up the XML parsing by installing
91
- [Nokogiri](https://rubygems.org/gems/nokogiri); otherwise the REXML from the
92
- Ruby standard library will be used.
93
+ [Nokogiri](https://rubygems.org/gems/nokogiri);
94
+ otherwise the REXML from the Ruby standard library will be used.
93
95
 
94
- Similarly, you can install either of the gems
95
- [EDTF](https://rubygems.org/gems/edtf) or
96
- [Chronic](https://rubygems.org/gems/chronic) to support a wide range of
97
- additional inputs for date variables.
96
+ Similarly, you can install the
97
+ [EDTF](https://rubygems.org/gems/edtf)
98
+ gem to support a wide range of additional inputs for date variables.
98
99
 
99
100
  CSL Styles and Locales
100
101
  ----------------------
101
- You can load CSL styles and locales by passing a respective XML string, file
102
- name, or URL. You can also load styles and locales by name if the
103
- corresponding files are installed in your local styles and locale directories.
102
+ You can load CSL styles and locales
103
+ by passing a respective XML string, file name, or URL.
104
+ You can also load styles and locales by name
105
+ if the corresponding files are installed
106
+ in your local styles and locale directories.
104
107
  By default, CSL-Ruby looks for CSL styles and locale files in
105
108
 
106
109
  /usr/local/share/csl/styles
107
110
  /usr/local/share/csl/locales
108
111
 
109
- You can change these locations by changing the value of `CSL::Style.root` and
110
- `CSL::Locale.root` respectively.
112
+ You can change these locations
113
+ by changing the value of `CSL::Style.root` and `CSL::Locale.root` respectively.
111
114
 
112
- Alternatively, you can `gem install csl-styles` to install all official CSL
113
- styles and locales. To make the styles and locales available, simply
114
- `require 'csl/styles`.
115
+ Alternatively, you can `gem install csl-styles`
116
+ to install all official CSL styles and locales.
117
+ To make the styles and locales available,
118
+ simply `require 'csl/styles'`.
115
119
 
116
120
  Development
117
121
  -----------
@@ -125,24 +129,28 @@ To get started, install the development dependencies and run all tests:
125
129
 
126
130
  $ cd citeproc-ruby
127
131
  $ bundle install
128
- $ rake
132
+ $ bundle exec rake
129
133
 
130
- If you've found a bug or have a question, please open an issue on the
134
+ If you've found a bug or have a question,
135
+ please open an issue on the
131
136
  [issue tracker](https://github.com/inukshuk/citeproc-ruby/issues).
132
- Or, for extra credit, clone the CiteProc-Ruby repository, write a failing
133
- example, fix the bug and submit a pull request.
137
+ Or, for extra credit, clone the CiteProc-Ruby repository,
138
+ write a failing example, fix the bug and submit a pull request.
134
139
 
135
140
  Credits
136
141
  -------
137
142
  Thanks to Rintze M. Zelle, Sebastian Karcher, Frank G. Bennett, Jr.,
138
- and Bruce D'Arcus of CSL and citeproc-js fame for their support!
143
+ and Bruce D'Arcus of CSL and citeproc-js fame
144
+ for their support!
139
145
 
140
- Thanks to Google and the Berkman Center at Harvard University for supporting
141
- this project as part of [Google Summer of Code](https://developers.google.com/open-source/soc/).
146
+ Thanks to Google and the Berkman Center at Harvard University
147
+ for supporting this project as part of
148
+ [Google Summer of Code](https://developers.google.com/open-source/soc/).
142
149
 
143
150
  Copyright
144
151
  ---------
145
- Copyright 2009-2024 Sylvester Keil. All rights reserved.
152
+ Copyright 2009-2026 Sylvester Keil.
153
+ All rights reserved.
146
154
 
147
155
  Copyright 2012 President and Fellows of Harvard College.
148
156
 
@@ -13,7 +13,7 @@ module CiteProc
13
13
  attr_reader :renderer, :style
14
14
 
15
15
  def_delegators :renderer,
16
- :format, :format=, :locale, :locale=
16
+ :format, :format=, :locale, :locale=, :clear_cache!
17
17
 
18
18
  def initialize(*arguments)
19
19
  super(*arguments)
@@ -22,8 +22,10 @@ module CiteProc
22
22
  update! unless processor.nil?
23
23
  end
24
24
 
25
+ # @param new_style [CSL::Style, String] a style or a style name, path,
26
+ # or URL; styles are used as is and never changed by the engine
25
27
  def style=(new_style)
26
- @style = CSL::Style.load new_style
28
+ @style = new_style.is_a?(CSL::Style) ? new_style : CSL::Style.load(new_style)
27
29
  end
28
30
 
29
31
  def process(data)
@@ -35,6 +37,7 @@ module CiteProc
35
37
  # populate item data
36
38
  data.each do |item|
37
39
  item.data = processor[item.id].dup
40
+ item.parse_locator!(renderer.locator_abbreviations)
38
41
  end
39
42
 
40
43
  # TODO implement sort in citation data
@@ -100,6 +103,7 @@ module CiteProc
100
103
 
101
104
  data.each do |item|
102
105
  item.data = processor[item.id].dup
106
+ item.parse_locator!(renderer.locator_abbreviations)
103
107
  end
104
108
 
105
109
  renderer.render_citation data, node
@@ -109,23 +113,15 @@ module CiteProc
109
113
  end
110
114
  end
111
115
 
112
-
116
+ # Applies the processor's format, locale, and style options.
117
+ #
118
+ # @return [self]
113
119
  def update!
120
+ renderer.clear_cache!
114
121
  renderer.format = processor.options[:format]
115
122
  renderer.locale = processor.options[:locale]
116
123
 
117
- if processor.options[:style].is_a? CSL::Style
118
- @style = processor.options[:style]
119
- else
120
- @style = CSL::Style.load processor.options[:style]
121
- end
122
-
123
- # Preliminary locale override implementation!
124
- # Does not yet reverse merge default region and default locale.
125
- @style.locales.sort.reverse.each do |locale|
126
- renderer.locale.merge! locale if renderer.locale.like?(locale)
127
- end
128
-
124
+ self.style = processor.options[:style]
129
125
  self
130
126
  end
131
127
 
@@ -7,7 +7,7 @@ module CiteProc
7
7
 
8
8
  @available = []
9
9
 
10
- @squeezable = /^[\s\.,:;!?\)\(\[\]]+$/
10
+ @squeezable = /^[\s\.,:;!?]+$/
11
11
 
12
12
  @stopwords = {
13
13
  :en => %w(a about above across afore after against along alongside amid amidst among amongst
@@ -16,7 +16,7 @@ module CiteProc
16
16
  forenenst from given in inside into lest like modulo near next nor notwithstanding of off
17
17
  on onto or out over per plus pro qua sans since so than the through thru throughout thruout
18
18
  till to toward towards under underneath until unto up upon versus vs v via vis-à-vis with
19
- within without)
19
+ within without yet de von van)
20
20
  }
21
21
 
22
22
  class << self
@@ -140,14 +140,16 @@ module CiteProc
140
140
  bibliography
141
141
  end
142
142
 
143
- def apply(input, node, locale = nil)
143
+ # @param language [String] the language of the item being formatted
144
+ def apply(input, node, locale = nil, language = nil)
144
145
  return '' if input.nil?
145
146
  return input if input.empty? || node.nil?
146
147
 
147
148
  return ArgumentError unless node.respond_to?(:formatting_options)
148
149
 
149
150
 
150
- @input, @output, @node, @locale = input, input.dup, node, locale
151
+ @input, @output, @node, @locale, @language =
152
+ input, input.dup, node, locale, language
151
153
 
152
154
  setup!
153
155
 
@@ -208,10 +210,10 @@ module CiteProc
208
210
  def apply_text_case
209
211
  case options[:'text-case']
210
212
  when 'lowercase'
211
- output.replace CiteProc.downcase output
213
+ output.replace turkic? ? output.downcase(:turkic) : CiteProc.downcase(output)
212
214
 
213
215
  when 'uppercase'
214
- output.replace CiteProc.upcase output
216
+ output.replace turkic? ? output.upcase(:turkic) : CiteProc.upcase(output)
215
217
 
216
218
  when 'capitalize-first'
217
219
  output.sub!(/^([^\p{Alnum}]*)(\p{Ll})/) { "#{$1}#{CiteProc.upcase($2)}" }
@@ -224,25 +226,34 @@ module CiteProc
224
226
  output.gsub!(/\b(\p{Lu})(\p{Lu}+)\b/) { "#{$1}#{CiteProc.downcase($2)}" }
225
227
 
226
228
  when 'title'
227
- return if locale && locale.language != :en
229
+ return unless english?
228
230
 
229
231
  # TODO add support for stop words consisting of multiple words
230
232
  #output.gsub!(/\b(\p{Lu})(\p{Lu}+)\b/) { "#{$1}#{CiteProc.downcase($2)}" }
231
233
 
232
234
  # TODO exceptions: word followed by colon
233
235
  first = true
234
- output.gsub!(/\b(\p{L})([\p{L}\.]+)\b/) do |word|
236
+ output.gsub!(/(?<!['’])\b(\p{L})([\p{L}\.]+)\b/) do |word|
235
237
  first_letter = $1
236
238
  rest_of_word = $2
239
+ before, after = Regexp.last_match.pre_match, Regexp.last_match.post_match
237
240
  result = word
238
241
 
239
- if first_letter.match(/^\p{Ll}/) && (!Format.stopword?(word) || first)
242
+ # Stop words are capitalized at the start of hyphenated words
243
+ capitalize = first || !Format.stopword?(word) ||
244
+ after.start_with?('-') && !before.end_with?('-')
245
+
246
+ # Words in mixed case like "iPad" are left as is
247
+ if first_letter.match(/^\p{Ll}/) && !rest_of_word.match?(/\p{Lu}/) && capitalize
240
248
  result = "#{CiteProc.upcase(first_letter)}#{rest_of_word}"
241
249
  end
242
250
  first = false
243
251
  result
244
252
  end
245
253
 
254
+ # Capitalize the first word after a colon
255
+ output.gsub!(/(:\s+)(\p{Ll})(?=\p{Ll}*\b)/) { "#{$1}#{CiteProc.upcase($2)}" }
256
+
246
257
  output.gsub!(/(\.|\b)(\p{Ll})([\p{L}\.]+)\b$/) do |word|
247
258
  word_boundary = $1
248
259
  first_letter = $2
@@ -257,6 +268,21 @@ module CiteProc
257
268
  end
258
269
  end
259
270
 
271
+ # Title case applies only to English items: items whose language
272
+ # is English or, if they have no language, when the locale is English.
273
+ def english?
274
+ if @language.nil? || @language.empty?
275
+ locale.nil? || locale.language == :en
276
+ else
277
+ @language.match?(/\Aen(-|\z)/i)
278
+ end
279
+ end
280
+
281
+ # @return [Boolean] whether the item's language uses Turkic case mappings
282
+ def turkic?
283
+ @language.to_s.match?(/\A(tr|az)(-|\z)/i)
284
+ end
285
+
260
286
  def punctuation_in_quotes?
261
287
  !locale.nil? && locale.punctuation_in_quotes?
262
288
  end
@@ -20,14 +20,15 @@ module CiteProc
20
20
  class Sort < Text
21
21
  #A special format to use when sorting which prevents formatting of extraneous things like quotes
22
22
 
23
- def apply(input, node, locale = nil)
23
+ def apply(input, node, locale = nil, language = nil)
24
24
  return '' if input.nil?
25
25
  return input if input.empty? || node.nil?
26
26
 
27
27
  return ArgumentError unless node.respond_to?(:formatting_options)
28
28
 
29
29
 
30
- @input, @output, @node, @locale = input, input.dup, node, locale
30
+ @input, @output, @node, @locale, @language =
31
+ input, input.dup, node, locale, language
31
32
 
32
33
  setup!
33
34
 
@@ -58,7 +58,7 @@ module CiteProc
58
58
  end
59
59
 
60
60
  if bibliography.hanging_indent?
61
- hanging_indent = "#{config[:bib_hanging_indent]}#{bib_unit}"
61
+ hanging_indent = "#{config[:bib_hanging_indent]}#{unit}"
62
62
 
63
63
  container_options['style']['padding-left'] = hanging_indent
64
64
  entry_options['style']['text-indent'] = "-#{hanging_indent}"
@@ -118,6 +118,14 @@ module CiteProc
118
118
  )
119
119
  end
120
120
 
121
+ # The suffix of a layout goes into a trailing display block
122
+ def apply_suffix
123
+ close = closing_tag(config[:display])
124
+ return super unless node.is_a?(CSL::Style::Layout) && output.end_with?(close)
125
+
126
+ output.replace "#{squeeze_suffix(output.delete_suffix(close), suffix)}#{close}"
127
+ end
128
+
121
129
  def prefix
122
130
  CSL.encode_xml_text(options[:prefix])
123
131
  end
@@ -131,7 +139,7 @@ module CiteProc
131
139
  end
132
140
 
133
141
  def strip(string)
134
- string.split(/((?:^<\p{Alpha}[^>]*>)|(?:<\/\p{Alpha}[^>]*>))$/, 2)
142
+ string.split(/((?:\A<\p{Alpha}[^>]*>)|(?:<\/\p{Alpha}[^>]*>))\z/, 2)
135
143
  end
136
144
 
137
145
  protected
@@ -194,6 +202,9 @@ module CiteProc
194
202
  end
195
203
 
196
204
  class CiteProcJS < Html
205
+ # The superscript characters converted to sup tags by citeproc-js
206
+ SUPERSCRIPTS = /[ª²-³¹-ºʰ-ʸˀ-ˁˠ-ˤۥ-ۦᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵡ⁰-ⁱ⁴-ⁿ℠™㆒-㆟]/
207
+
197
208
  def initialize
198
209
  super(
199
210
  :bib_container => 'div',
@@ -222,6 +233,35 @@ module CiteProc
222
233
  bibliography.connector = indent ? "\n" : ''
223
234
  bibliography
224
235
  end
236
+
237
+ def apply_vertical_align
238
+ case options[:'vertical-align']
239
+ when 'sup', 'sub'
240
+ output.replace content_tag(options[:'vertical-align'], output)
241
+ else
242
+ super
243
+ end
244
+ end
245
+
246
+ protected
247
+
248
+ def finalize!
249
+ super
250
+
251
+ # Use typographic apostrophes
252
+ output.gsub!(/(?<=\p{L})'|'(?=\d)/, '’')
253
+
254
+ output.gsub!(SUPERSCRIPTS) do |char|
255
+ content_tag('sup', char.unicode_normalize(:nfkc))
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ def style_for(options)
262
+ return unless options && !options.empty?
263
+ options.map { |k, v| "#{k}:#{v};" }.join
264
+ end
225
265
  end
226
266
 
227
267
  end
@@ -3,39 +3,84 @@ module CiteProc
3
3
 
4
4
  class Renderer
5
5
 
6
+ # The date parts from largest to smallest
7
+ DATE_PARTS = %w{ year month day }.freeze
8
+
6
9
  # @param item [CiteProc::CitationItem]
7
10
  # @param node [CSL::Node]
8
- # @raise RenderingError
11
+ # @raise CSL::Error if the locale has no date in the form of the node
9
12
  # @return [String]
10
13
  def render_date(item, node)
11
14
  return '' unless node.has_variable?
12
15
 
13
- date = item.data[node.variable]
14
- return '' if date.nil? || date.empty?
16
+ # Observers see the date as rendered:
17
+ # a date without the date parts used counts as empty
18
+ item.data.deferred_read_attribute(node.variable) do |date|
19
+ next '' if date.nil? || date.empty?
20
+ next date.to_s if date.literal?
15
21
 
16
- return date.to_s if date.literal?
22
+ parts, delimiter = node.parts_for(locale), node.delimiter_for(locale)
17
23
 
18
- # TODO date-ranges
24
+ if date.range?
25
+ render_date_range date, parts, delimiter
26
+ else
27
+ render_date_parts date, parts, delimiter
28
+ end
29
+ end
30
+ end
19
31
 
20
- if node.localized?
21
- localized_node = locale.date.detect { |d| d.form == node.form } or
22
- raise RenderingError, "no localized date for form #{node.form} found"
32
+ # Renders a date range. The date parts which are the same for
33
+ # both dates are rendered only once; the date parts which differ
34
+ # are rendered for both dates and are joined by the range delimiter
35
+ # of the largest date part that differs.
36
+ #
37
+ # @param date [CiteProc::Date] the date range
38
+ # @param parts [Array<CSL::Style::DatePart, CSL::Locale::DatePart>]
39
+ # @param delimiter [String]
40
+ # @return [String]
41
+ def render_date_range(date, parts, delimiter)
42
+ from, to = date.parts
23
43
 
24
- delimiter, filter = node.delimiter, node.parts_filter
44
+ # Skip date parts without values (e.g., days in a month range)
45
+ parts = parts.select { |part| from[part.name] || to[part.name] }
25
46
 
26
- parts = localized_node.parts.select do |part|
27
- filter.include? part.name
28
- end
29
- else
30
- parts, delimiter = node.parts, node.delimiter
47
+ # Open ranges are rendered with a trailing range delimiter
48
+ return [
49
+ render_date_parts(from, parts, delimiter),
50
+ range_delimiter_for(parts, 'year')
51
+ ].join('') if date.open_range?
52
+
53
+ names = parts.map(&:name)
54
+ largest = DATE_PARTS.detect do |name|
55
+ names.include?(name) && from[name] != to[name]
31
56
  end
32
57
 
58
+ return render_date_parts(from, parts, delimiter) if largest.nil?
59
+
60
+ # If one date is more precise than the other, all parts differ
61
+ differing = names.any? { |name| from[name].nil? != to[name].nil? } ?
62
+ DATE_PARTS :
63
+ DATE_PARTS.drop_while { |name| name != largest }
64
+
65
+ before, range, after = split_date_parts(parts, differing) || [[], parts, []]
66
+
67
+ [
68
+ render_date_parts(from, before, delimiter),
69
+ [
70
+ render_date_parts(from, without_suffix(range), delimiter),
71
+ render_date_parts(to, range, delimiter)
72
+ ].join(range_delimiter_for(parts, largest)),
73
+ render_date_parts(from, after, delimiter)
74
+ ].reject(&:empty?).join(delimiter)
75
+ end
76
+
77
+ def render_date_parts(date, parts, delimiter)
33
78
  parts.map { |part|
34
79
  render date, part
35
80
  }.reject(&:empty?).join(delimiter)
36
81
  end
37
82
 
38
- # @param date [CiteProc::Date]
83
+ # @param date [CiteProc::Date, CiteProc::Date::DateParts]
39
84
  # @param node [CSL::Style::DatePart, CSL::Locale::DatePart]
40
85
  # @return [String]
41
86
  def render_date_part(date, node)
@@ -48,7 +93,8 @@ module CiteProc
48
93
  if date.day > 1 && locale.limit_day_ordinals?
49
94
  date.day.to_s
50
95
  else
51
- ordinalize date.day
96
+ # The ordinal uses the gender of the month
97
+ ordinalize date.day, :noun => date.month && 'month-%02d' % date.month
52
98
  end
53
99
  when node.form == 'numeric-leading-zeros'
54
100
  '%02d' % date.day
@@ -58,6 +104,8 @@ module CiteProc
58
104
 
59
105
  when node.month?
60
106
  case
107
+ when date.season.is_a?(String)
108
+ date.season
61
109
  when date.season?
62
110
  translate(('season-%02d' % date.season), node.attributes_for(:form))
63
111
  when date.month.nil?
@@ -91,6 +139,42 @@ module CiteProc
91
139
  end
92
140
  end
93
141
 
142
+ private
143
+
144
+ # @return [String] the range delimiter set on the date part
145
+ # with the given name; defaults to an en-dash
146
+ def range_delimiter_for(parts, name)
147
+ part = parts.detect { |p| p.name == name }
148
+ part && part[:'range-delimiter'] || '–'
149
+ end
150
+
151
+ # Splits the date parts into the common parts before, the
152
+ # differing parts, and the common parts after them.
153
+ #
154
+ # @return [Array<Array>, nil] the split parts or nil if the
155
+ # differing parts are not adjacent
156
+ def split_date_parts(parts, differing)
157
+ first = parts.index { |part| differing.include?(part.name) }
158
+ last = parts.rindex { |part| differing.include?(part.name) }
159
+ range = parts[first..last]
160
+
161
+ return unless range.all? { |part| differing.include?(part.name) }
162
+
163
+ [parts[0...first], range, parts[(last + 1)..]]
164
+ end
165
+
166
+ # @return [Array<CSL::Node>] the date parts with a copy
167
+ # of the last date part without its suffix
168
+ def without_suffix(parts)
169
+ last = parts[-1]
170
+ return parts unless last.attribute?(:suffix)
171
+
172
+ copy = last.deep_copy
173
+ copy[:suffix] = nil
174
+
175
+ [*parts[0...-1], copy]
176
+ end
177
+
94
178
  end
95
179
 
96
180
  end