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.
@@ -16,7 +16,16 @@ module CiteProc
16
16
  # Applies the current format on the string using the
17
17
  # node's formatting options.
18
18
  def format!(string, node)
19
- format.apply(string, node, locale)
19
+ format.apply(string, node, locale, state.language)
20
+ end
21
+
22
+ # Reads the item's language without notifying observers.
23
+ #
24
+ # @param item [CiteProc::Item, nil]
25
+ # @return [String, nil] the item's language
26
+ def language_of(item)
27
+ return unless item.respond_to?(:unobservable_read_attribute)
28
+ item.unobservable_read_attribute(:language).to_s
20
29
  end
21
30
 
22
31
  def join(list, delimiter = nil)
@@ -42,93 +51,19 @@ module CiteProc
42
51
  end
43
52
 
44
53
 
45
- # Formats pages according to format. Valid formats are:
46
- #
47
- # * "chicago": page ranges are abbreviated according to
48
- # the Chicago Manual of Style rules.
49
- # * "expanded": Abbreviated page ranges are expanded to
50
- # their non-abbreviated form: 42-45, 321-328, 2787-2816.
51
- # * "minimal": All digits repeated in the second number
52
- # are left out: 42-45, 321-8, 2787-816.
53
- #
54
- # @param [String] pages to be formatted
55
- # @param [String] format to use for formatting
54
+ # @param pages [String] the pages to format
55
+ # @param format [String, nil] the page range format
56
+ # @return [String, nil] the pages formatted according to format
57
+ # using the localized range delimiter
56
58
  def format_page_range(pages, format)
57
- return if pages.nil?
58
- format_page_range!(pages.dup, format)
59
+ CiteProc::Number.format_page_range(pages, format, range_delimiter)
59
60
  end
60
61
 
61
- def format_page_range!(pages, format)
62
- return if pages.nil?
63
- return pages if pages.empty?
64
-
65
- dash = translate('page-range-delimiter') || '–' # en-dash
66
-
67
- pages.gsub! PAGE_RANGE_PATTERN do
68
- affixes, f, t = [$1, $3, $4, $6], $2, $5
69
-
70
- # When there are affixes or no format was
71
- # specified we skip this part. As a result,
72
- # only the delimiter will be replaced!
73
-
74
- if affixes.all?(&:empty?) && !format.nil?
75
-
76
- dim = f.length
77
- delta = dim - t.length
78
-
79
- if delta >= 0
80
- t.prepend f[0, delta] unless delta.zero?
81
-
82
- # TODO
83
- if format == 'chicago-16' || format == 'chicago-15'
84
- format = 'chicago'
85
- end
86
-
87
- if format == 'chicago'
88
- changes = dim - f.chars.zip(t.chars).
89
- take_while { |a,b| a == b }.length if dim == 4
90
-
91
- format = case
92
- when dim < 3
93
- 'expanded'
94
- when dim == 4 && changes > 2
95
- 'expanded'
96
- when f[-2, 2] == '00'
97
- 'expanded'
98
- when f[-2] == '0'
99
- 'minimal'
100
- else
101
- 'minimal-two'
102
- end
103
- end
104
-
105
- case format
106
- when 'expanded'
107
- # nothing to do
108
- when 'minimal'
109
- t = t.each_char.drop_while.with_index { |c, i| c == f[i] }.join('')
110
- when 'minimal-two'
111
- if dim > 2
112
- t = t.each_char.drop_while.with_index { |c, i|
113
- c == f[i] && dim - i > 2
114
- }.join('')
115
- end
116
- else
117
- raise ArgumentError, "unknown page range format: #{format}"
118
- end
119
- end
120
- end
121
-
122
- affixes.zip([f, dash, t]).flatten.compact.join('')
123
- end
124
-
125
- pages
62
+ # @return [String] the localized range delimiter; defaults to an en-dash
63
+ def range_delimiter
64
+ translate('page-range-delimiter') || '–'
126
65
  end
127
66
 
128
- PAGE_RANGE_PATTERN =
129
- # ------------ -2- ------------ ------------ -5- ------------
130
- /\b([[:alpha:]]*)(\d+)([[:alpha:]]*)\s*[–-]+\s*([[:alpha:]]*)(\d+)([[:alpha:]]*)\b/
131
-
132
67
  end
133
68
 
134
69
  end
@@ -7,24 +7,36 @@ module CiteProc
7
7
  # @param node [CSL::Style::Group]
8
8
  # @return [String]
9
9
  def render_group(item, node)
10
- return '' unless node.has_children?
10
+ render_as_group item, node, node.delimiter
11
+ end
12
+
13
+ private
11
14
 
15
+ # Renders the node's children with group suppression:
16
+ # the result is empty if the children call variables
17
+ # and ALL of them are empty; a non-empty result counts
18
+ # as a non-empty variable for any enclosing groups.
19
+ #
20
+ # @param item [CiteProc::CitationItem]
21
+ # @param node [CSL::Node]
22
+ # @param delimiter [String, nil]
23
+ # @return [String]
24
+ def render_as_group(item, node, delimiter = nil)
12
25
  observer = ItemObserver.new(item.data)
13
26
  observer.start
14
27
 
15
28
  begin
16
- rendition = node.each_child.map { |child|
29
+ rendition = join(node.each_child.map { |child|
17
30
  render item, child
18
- }.reject(&:empty?)
19
-
20
- rendition = join(rendition, node.delimiter)
21
-
31
+ }.reject(&:empty?), delimiter)
22
32
  ensure
23
33
  observer.stop
24
34
  end
25
35
 
26
36
  return '' if observer.skip?
27
37
 
38
+ item.data.simulate_read_attribute :group, rendition unless rendition.empty?
39
+
28
40
  rendition
29
41
  end
30
42
 
@@ -15,8 +15,6 @@ module CiteProc
15
15
  when node.page?
16
16
  value, name = item.read_attribute(:page) || item.data.unobservable_read_attribute(:page).to_s, :page
17
17
 
18
- format_page_range!(value, node.page_range_format)
19
-
20
18
  when node.locator?
21
19
 
22
20
  # Subtle: when there is no locator we also look
@@ -52,6 +50,8 @@ module CiteProc
52
50
  false
53
51
  when node.number_of_pages?, node.number_of_volumes?
54
52
  value.to_i > 1
53
+ when node.locator?
54
+ item.plural_locator?(locator_abbreviations)
55
55
  when value.respond_to?(:plural?)
56
56
  value.plural?
57
57
  else
@@ -5,13 +5,16 @@ module CiteProc
5
5
 
6
6
  private
7
7
 
8
+ # Note that the layout's delimiter is used only between
9
+ # cites in a citation, not between the layout's children.
10
+ #
8
11
  # @param item [CiteProc::CitationItem]
9
12
  # @param node [CSL::Style::Layout]
10
13
  # @return [String]
11
14
  def render_layout(item, node)
12
15
  join node.each_child.map { |child|
13
16
  render item, child
14
- }.reject(&:empty?), node.delimiter
17
+ }.reject(&:empty?)
15
18
  end
16
19
 
17
20
  end
@@ -3,12 +3,16 @@ module CiteProc
3
3
 
4
4
  class Renderer
5
5
 
6
+ # @return [CSL::Locale] the locale including the in-style locale
7
+ # definitions of the style currently being rendered
6
8
  def locale
7
- @locale ||= CSL::Locale.load
9
+ localize(base_locale)
8
10
  end
9
11
 
12
+ # @param locale [CSL::Locale, String] a locale or an IETF tag;
13
+ # locales are used as is and never changed by the renderer
10
14
  def locale=(locale)
11
- @locale = CSL::Locale.load(locale)
15
+ @locale = locale.is_a?(CSL::Locale) ? locale : CSL::Locale.load(locale)
12
16
  end
13
17
 
14
18
  def translate(name, options = {})
@@ -20,6 +24,63 @@ module CiteProc
20
24
  locale.ordinalize(number, options)
21
25
  end
22
26
 
27
+ # @return [Hash<String,String>] the abbreviations of locator labels
28
+ # including the localized short terms
29
+ def locator_abbreviations
30
+ @locator_abbreviations ||= {}.compare_by_identity
31
+ @locator_abbreviations[locale] ||= begin
32
+ terms = {}
33
+
34
+ CiteProc::CitationItem.labels.each do |label|
35
+ [false, true].each do |plural|
36
+ term = translate(label, :form => 'short', :plural => plural)
37
+ terms[term] = label.to_s unless term.to_s.empty?
38
+ end
39
+ end
40
+
41
+ CiteProc::CitationItem.locator_abbreviations.merge(terms)
42
+ end
43
+ end
44
+
45
+ # @return [String] the symbol form of the "and" term
46
+ # if the locale defines it; otherwise "&"
47
+ def ampersand
48
+ term = locale.terms.lookup('and', :form => 'symbol')
49
+ term&.form == 'symbol' ? term.to_s : '&'
50
+ end
51
+
52
+ private
53
+
54
+ def clear_locale_cache!
55
+ @localized, @locales, @locator_abbreviations = nil, nil, nil
56
+ end
57
+
58
+ # @return [CSL::Locale] the locale without in-style locale
59
+ # definitions; defaults to the default locale
60
+ def base_locale
61
+ @locale ||= CSL::Locale.load
62
+ end
63
+
64
+ # Localized copies are cached per style and locale; the
65
+ # style and the locale themselves are never changed.
66
+ #
67
+ # @param locale [CSL::Locale]
68
+ # @return [CSL::Locale] the locale localized for the current style
69
+ def localize(locale)
70
+ return locale if style.nil?
71
+
72
+ @localized ||= {}.compare_by_identity
73
+ localized = (@localized[style] ||= {}.compare_by_identity)
74
+ localized[locale] ||= style.localize(locale)
75
+ end
76
+
77
+ # @param language [String] an IETF tag
78
+ # @return [CSL::Locale] the (cached) locale for the language
79
+ def locale_for(language)
80
+ @locales ||= {}
81
+ @locales[language] ||= CSL::Locale.load(language)
82
+ end
83
+
23
84
  end
24
85
 
25
86
  end
@@ -6,12 +6,10 @@ module CiteProc
6
6
  private
7
7
 
8
8
  # @param item [CiteProc::CitationItem]
9
- # @param node [CSL::Style::Layout]
9
+ # @param node [CSL::Style::Macro]
10
10
  # @return [String]
11
11
  def render_macro(item, node)
12
- node.each_child.map { |child|
13
- render item, child
14
- }.join('')
12
+ render_as_group item, node
15
13
  end
16
14
 
17
15
  end
@@ -25,7 +25,8 @@ module CiteProc
25
25
 
26
26
  rendered_names = render_substitute item, node.substitute
27
27
 
28
- if substitute_subsequent_authors_completely? &&
28
+ # Substituted output is replaced as a whole for all rules
29
+ if substitute_subsequent_authors? &&
29
30
  completely_substitute?(rendered_names)
30
31
 
31
32
  rendered_names = state.node.subsequent_author_substitute
@@ -146,8 +147,11 @@ module CiteProc
146
147
  connector = node.connector
147
148
  connector = translate('and') if connector == 'text'
148
149
 
149
- # Add spaces around connector
150
- connector = " #{connector} " unless connector.nil?
150
+ # Add spaces around connector unless it belongs to a
151
+ # script which does not separate words by spaces
152
+ if CiteProc.romanesque_start?(connector)
153
+ connector = " #{connector} "
154
+ end
151
155
 
152
156
  rendered_names =
153
157
  case
@@ -171,12 +175,14 @@ module CiteProc
171
175
  ], node.ellipsis
172
176
 
173
177
  else
174
- others = node.et_al ?
175
- format!(translate(node.et_al[:term]), node.et_al) :
176
- translate('et-al')
178
+ term = translate(node.et_al ? node.et_al[:term] : 'et-al')
179
+ others = node.et_al ? format!(term, node.et_al) : term
177
180
 
178
- connector = node.delimiter_precedes_et_al?(truncated) ?
179
- delimiter : ' '
181
+ connector = case
182
+ when node.delimiter_precedes_et_al?(truncated) then delimiter
183
+ when CiteProc.romanesque_start?(term) then ' '
184
+ else ''
185
+ end
180
186
 
181
187
  join [
182
188
  join(truncated.map.with_index { |name, idx|
@@ -185,7 +191,7 @@ module CiteProc
185
191
 
186
192
  others
187
193
 
188
- ], connector
194
+ ].reject { |part| part.to_s.empty? }, connector
189
195
 
190
196
  end
191
197
 
@@ -220,7 +226,7 @@ module CiteProc
220
226
  rendered_names = state.node.subsequent_author_substitute
221
227
  end
222
228
 
223
- format! rendered_names, node
229
+ affix rendered_names, node
224
230
  end
225
231
 
226
232
  # @param names [CiteProc::Name]
@@ -231,10 +237,6 @@ module CiteProc
231
237
  if name.personal?
232
238
  name = name.dup
233
239
 
234
- # TODO move parts of the formatting logic here
235
- # because name parts may include particles etc.
236
-
237
-
238
240
  # Strip away some unusual characters to normalize
239
241
  # sort order for names.
240
242
  if sort_mode?
@@ -244,65 +246,26 @@ module CiteProc
244
246
  name.options.merge! node.name_options
245
247
  name.sort_order! node.name_as_sort_order_at?(position)
246
248
 
247
- name.initialize_without_hyphen! if node.initialize_without_hyphen?
249
+ name.initialize_without_hyphen! if style && style.initialize_without_hyphen?
248
250
 
249
251
  if style && style.demote_particle?
250
252
  name.options[:'demote-non-dropping-particle'] = style.demote_particle
251
253
  end
252
254
 
253
- # Strip away (hyphenated) particles in sort mode!
254
- if sort_mode? && name.demote_particle?
255
- name.family = name.family.to_s.sub(/^[[:lower:]]+[\s-]/, '')
256
- end
257
-
258
- node.name_part.each do |part|
259
- case part[:name]
260
- when 'family'
261
- if !name.particle? || name.demote_particle?
262
- name.family = format!(name.family, part)
263
- else
264
- name.family = format!("#{name.particle} #{name.family}", part)
265
- name.particle = nil
266
- end
267
-
268
- # Name suffix must be enclosed by family-part
269
- # suffix in display order!
270
- if name.has_suffix? && !name.sort_order? && part.attribute?(:suffix)
271
- comma = name.comma_suffix? ? name.comma : ' '
272
- suffix = part[:suffix]
273
-
274
- name.family.chomp! suffix
275
- name.family.concat "#{comma}#{name.suffix}#{suffix}"
276
- name.suffix = nil
277
- end
278
-
279
- when 'given'
280
- if name.dropping_particle?
281
- name.given = format!("#{name.initials} #{name.dropping_particle}", part)
282
- name.dropping_particle = nil
283
- else
284
- name.given = format!(name.initials, part)
285
- end
286
-
287
- # Demoted particles must be enclosed by
288
- # given-part affixes in sort order!
289
- if name.particle? && name.demote_particle? &&
290
- name.sort_order? && part.attribute?(:suffix)
255
+ # Particles are demoted in sort keys unless they are never demoted
256
+ name.always_demote_particle! if sort_mode? && !name.never_demote_particle?
291
257
 
292
- suffix = part[:suffix]
293
-
294
- name.given.chomp! suffix
295
- name.given.concat " #{name.particle}#{suffix}"
258
+ end
296
259
 
297
- name.particle = nil
298
- end
260
+ parts = node.name_part.to_h { |part| [part[:name].to_sym, part] }
299
261
 
300
- end
301
- end
302
-
303
- end
262
+ formatted = name.format(
263
+ ->(part, text) { format_without_affixes(text, parts[part]) },
264
+ ->(part, text) { affix(text, parts[part]) }
265
+ )
304
266
 
305
- format! name.format, node
267
+ # The affixes of the name node enclose the list of names
268
+ format_without_affixes formatted, node
306
269
  end
307
270
 
308
271
  # @param item [CiteProc::CitationItem]
@@ -381,6 +344,23 @@ module CiteProc
381
344
  name
382
345
  end
383
346
 
347
+ # @return [String, nil] the text with the formatting, but
348
+ # without the affixes, of the node
349
+ def format_without_affixes(text, node)
350
+ return text if node.nil? || text.to_s.empty?
351
+
352
+ formatting = node.deep_copy
353
+ formatting[:prefix] = formatting[:suffix] = nil
354
+
355
+ format! text, formatting
356
+ end
357
+
358
+ # @return [String, nil] the text enclosed by the affixes of the node
359
+ def affix(text, node)
360
+ return text if node.nil? || text.to_s.empty?
361
+ "#{node[:prefix]}#{text}#{node[:suffix]}"
362
+ end
363
+
384
364
  def resolve_editor_translator_exception!(names)
385
365
 
386
366
  i = names.index { |role, _| role == :translator }
@@ -391,6 +371,9 @@ module CiteProc
391
371
 
392
372
  return names unless names[i][1] == names[j][1]
393
373
 
374
+ # An empty term disables the special case
375
+ return names if translate('editortranslator').to_s.empty?
376
+
394
377
  # rename the first instance and drop the second one
395
378
  i, j = j, i if j < i
396
379
 
@@ -16,8 +16,7 @@ module CiteProc
16
16
 
17
17
  case
18
18
  when node.ordinal? || node.long_ordinal?
19
- options = node.attributes_for :form
20
- # TODO lookup term of variable to check gender
19
+ options = node.attributes_for(:form).merge(:noun => node.variable)
21
20
 
22
21
  numbers.map! do |num|
23
22
  num =~ /^\d+$/ ? ordinalize(num, options) : num
@@ -32,6 +31,7 @@ module CiteProc
32
31
  # nothing
33
32
  end
34
33
 
34
+ numbers.map! { |num| num == '-' ? range_delimiter : num }
35
35
  numbers.join('')
36
36
  end
37
37
 
@@ -20,14 +20,14 @@ module CiteProc
20
20
  self
21
21
  end
22
22
 
23
+ # A variable read more than once counts as non-empty
24
+ # if any of the reads was non-empty
23
25
  def update(method, key, value)
24
- history[key] = value if method == :read
26
+ history[key] = value if method == :read && empty?(history[key])
25
27
  end
26
28
 
27
29
  def skip?
28
- !history.empty? && history.values.all? { |v|
29
- v.nil? || v.respond_to?(:empty?) && v.empty?
30
- }
30
+ !history.empty? && history.values.all? { |v| empty?(v) }
31
31
  end
32
32
 
33
33
  def accessed
@@ -38,6 +38,12 @@ module CiteProc
38
38
  history.clear
39
39
  self
40
40
  end
41
+
42
+ private
43
+
44
+ def empty?(value)
45
+ value.nil? || value.respond_to?(:empty?) && value.empty?
46
+ end
41
47
  end
42
48
 
43
49
  end
@@ -30,6 +30,9 @@ module CiteProc
30
30
 
31
31
  attr_reader :history, :node, :item, :authors, :substitute
32
32
 
33
+ # @return [String, nil] the language of the item being rendered
34
+ attr_accessor :language
35
+
33
36
  def initialize
34
37
  @history, @authors = History.new(self, 3), []
35
38
  reset
@@ -55,7 +58,8 @@ module CiteProc
55
58
  end
56
59
 
57
60
  def reset
58
- @item, @node, @substitute, @authors, @names = nil, nil, nil, [], nil
61
+ @item, @node, @substitute, @authors, @names, @language =
62
+ nil, nil, nil, [], nil, nil
59
63
  self
60
64
  ensure
61
65
  changed
@@ -16,7 +16,12 @@ module CiteProc
16
16
  # in item.data; there should be no locator there
17
17
  # either but the read access will be noticed by
18
18
  # observers (if any).
19
- text = item.locator || item.data[:locator].to_s
19
+ text = (item.locator || item.data[:locator]).to_s
20
+
21
+ elsif node.variable == 'year-suffix'
22
+ # The year-suffix is set by the processor (for disambiguation)
23
+ # and does not count as a variable for group suppression
24
+ text = item.data.unobservable_read_attribute(:'year-suffix').to_s
20
25
 
21
26
  else
22
27
  text = item.data.variable(node.variable, node.variable_options).to_s
@@ -25,7 +30,8 @@ module CiteProc
25
30
  context, was_short_form = node.variable.split(/-short$/, 2)
26
31
 
27
32
  if !was_short_form.nil? || node[:form] == 'short'
28
- if text.empty? && context != node.variable
33
+ # Only the short form falls back to the long form
34
+ if text.empty? && context != node.variable && node[:form] == 'short'
29
35
  text = item.data.variable(context, node.variable_options).to_s
30
36
  end
31
37
 
@@ -36,7 +42,14 @@ module CiteProc
36
42
 
37
43
  case
38
44
  when node.variable == 'page'
39
- format_page_range!(text, node.page_range_format)
45
+ text = format_page_range(text, node.page_range_format)
46
+
47
+ when node.variable == 'locator'
48
+ format = node.page_range_format if item.label == 'page'
49
+ text = format_page_range(text, format)
50
+
51
+ # Use the localized ampersand (e.g., in "213 & 235")
52
+ text = text.gsub(/\s*&\s*/, " #{ampersand} ")
40
53
 
41
54
  when node.variable == 'page-first' && text.empty?
42
55
  text = item.data[:'page'].to_s[/\d+/].to_s
@@ -26,6 +26,15 @@ module CiteProc
26
26
  end
27
27
  alias abbrev abbreviate
28
28
 
29
+ # Clears the renderer's caches. Call this after changing styles or
30
+ # locales in place, or to free memory when using many different styles.
31
+ #
32
+ # @return [self]
33
+ def clear_cache!
34
+ clear_locale_cache!
35
+ self
36
+ end
37
+
29
38
  def allow_locale_overrides?
30
39
  return false unless engine
31
40
  engine.options[:allow_locale_overrides]
@@ -65,10 +74,14 @@ module CiteProc
65
74
  # @param node [CSL::Style::Layout]
66
75
  # @return [String] the rendered and string
67
76
  def render_single_citation(item, node)
77
+ state.language = language_of(item.data)
78
+
68
79
  # TODO author_only
69
80
  item.suppress! 'author' if item.suppress_author?
70
81
 
71
82
  join [item.prefix, render_layout(item, node), item.suffix].compact
83
+ ensure
84
+ state.language = nil
72
85
  end
73
86
 
74
87
  # @param item [CiteProc::CitationItem]
@@ -76,20 +89,37 @@ module CiteProc
76
89
  # @return [String] the rendered and formatted string
77
90
  def render_bibliography(item, node)
78
91
  state.store! item, node
92
+ state.language = language_of(item.data)
79
93
 
80
- if allow_locale_overrides? && item.language != locale.language
94
+ language = item.language.to_s
95
+
96
+ if allow_locale_overrides? && !language.empty?
81
97
  begin
82
- new_locale = CSL::Locale.load(item.language)
98
+ item_locale = locale_for(language)
83
99
 
84
- unless new_locale.nil?
85
- original_locale, @locale = @locale, new_locale
100
+ if item_locale != base_locale
101
+ original_locale, @locale = @locale, item_locale
86
102
  end
87
103
  rescue ParseError
88
104
  # locale not found
89
105
  end
90
106
  end
91
107
 
92
- result = render item, node.layout
108
+ fields = node.layout.each_child.map { |child|
109
+ render item, child
110
+ }.reject(&:empty?)
111
+
112
+ # With second-field-align, the first field is rendered
113
+ # in the left margin and the other fields inline
114
+ # (without trailing whitespace)
115
+ if node.second_field_align? && fields.length > 1
116
+ fields = [
117
+ format!(fields[0], CSL::Style::Group.new(:display => 'left-margin')),
118
+ format!(join(fields[1..]).rstrip, CSL::Style::Group.new(:display => 'right-inline'))
119
+ ]
120
+ end
121
+
122
+ result = format! join(fields), node.layout
93
123
 
94
124
  ensure
95
125
  unless original_locale.nil?
@@ -101,6 +131,7 @@ module CiteProc
101
131
 
102
132
  def render_sort(item, node, key)
103
133
  state.store! nil, key
134
+ state.language = language_of(item)
104
135
 
105
136
  original_format = @format
106
137
  @format = Formats::Sort.new
@@ -1,5 +1,5 @@
1
1
  module CiteProc
2
2
  module Ruby
3
- VERSION = '2.2.0'
3
+ VERSION = '2.3.0'
4
4
  end
5
5
  end