citeproc-ruby 2.1.8 → 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.
@@ -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?
@@ -99,26 +129,24 @@ module CiteProc
99
129
  state.clear! result
100
130
  end
101
131
 
102
- def render_sort(a, b, node, key)
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
107
138
 
108
- if a.is_a?(CiteProc::Names)
109
- [render_name(a, node), render_name(b, node)]
139
+ if item.is_a?(CiteProc::Names)
140
+ render_name(item, node)
110
141
 
111
142
  else
112
143
  # We need to clear any items that are suppressed
113
144
  # because they were used as substitutes during
114
145
  # rendering for sorting purposes!
115
- a_rendered = render a.cite, node
116
- a.suppressed.clear
117
-
118
- b_rendered = render b.cite, node
119
- b.suppressed.clear
146
+ rendered = render item.cite, node
147
+ item.suppressed.clear
120
148
 
121
- [a_rendered, b_rendered]
149
+ rendered
122
150
  end
123
151
 
124
152
  ensure
@@ -1,3 +1,9 @@
1
+ begin
2
+ require 'ffi-icu'
3
+ rescue LoadError
4
+ # ignore
5
+ end
6
+
1
7
  module CiteProc
2
8
  module Ruby
3
9
 
@@ -6,24 +12,30 @@ module CiteProc
6
12
  def sort!(items, keys)
7
13
  return items unless !keys.nil? && !keys.empty?
8
14
 
15
+ # Sort values are computed only once per item
16
+ # and key for the duration of the sort.
17
+ cache = {}.compare_by_identity
18
+
9
19
  # TODO refactor
10
20
  if items.is_a?(CitationData)
11
21
  items.sort! do |a, b|
12
- compare_items_by_keys(a.data, b.data, keys)
22
+ compare_items_by_keys(a.data, b.data, keys, cache)
13
23
  end
14
24
  else
15
25
  items.sort! do |a, b|
16
- compare_items_by_keys(a, b, keys)
26
+ compare_items_by_keys(a, b, keys, cache)
17
27
  end
18
28
  end
19
29
  end
20
30
 
31
+ private
32
+
21
33
  # @returns [-1, 0, 1, nil]
22
- def compare_items_by_keys(a, b, keys)
34
+ def compare_items_by_keys(a, b, keys, cache)
23
35
  result = 0
24
36
 
25
37
  keys.each do |key|
26
- result = compare_items_by_key(a, b, key)
38
+ result = compare_items_by_key(a, b, key, cache)
27
39
  return result unless result.zero?
28
40
  end
29
41
 
@@ -31,52 +43,83 @@ module CiteProc
31
43
  end
32
44
 
33
45
  # @returns [-1, 0, 1, nil]
34
- def compare_items_by_key(a, b, key)
35
- if key.macro?
36
- result = compare_items(*renderer.render_sort(a, b, key.macro, key))
46
+ def compare_items_by_key(a, b, key, cache)
47
+ va, vb = sort_value(a, key, cache), sort_value(b, key, cache)
37
48
 
38
- else
39
- va, vb = a[key.variable], b[key.variable]
40
-
41
- # Return early if one side is nil. In this
42
- # case ascending/descending is irrelevant!
43
- return 1 if va.nil? || va.empty?
44
- return -1 if vb.nil? || vb.empty?
45
-
46
- result = case CiteProc::Variable.types[key.variable]
47
- when :names
48
- node = CSL::Style::Name.new(key.name_options)
49
- node.all_names_as_sort_order!
50
-
51
- compare_items(*renderer.render_sort(va, vb, node, key))
52
-
53
- when :date
54
- va <=> vb
55
- when :number
56
- va <=> vb
57
- else
58
- compare_items(va, vb)
59
- end
60
- end
49
+ # Return early if either side is nil.
50
+ # In this case ascending/descending is irrelevant!
51
+ return 0 if va.nil? && vb.nil?
52
+ return 1 if va.nil?
53
+ return -1 if vb.nil?
61
54
 
55
+ result = va <=> vb
62
56
  result = -result unless key.ascending?
63
57
  result
64
58
  end
65
59
 
66
- def compare_items(a, b)
67
- if sort_case_sensitively?
68
- a <=> b
60
+ def sort_value(item, key, cache)
61
+ values = (cache[item] ||= {})
62
+ values.fetch(key) { values[key] = compute_sort_value(item, key) }
63
+ end
64
+
65
+ def compute_sort_value(item, key)
66
+ if key.macro?
67
+ return sort_key(renderer.render_sort(item, key.macro, key))
68
+ end
69
+
70
+ value = item[key.variable]
71
+ return if value.nil? || value.empty?
72
+
73
+ case CiteProc::Variable.types[key.variable]
74
+ when :names
75
+ node = CSL::Style::Name.new(key.name_options)
76
+ node.all_names_as_sort_order!
77
+
78
+ sort_key(renderer.render_sort(value, node, key))
79
+
80
+ when :date, :number
81
+ value
69
82
  else
70
- a.to_s.downcase <=> b.to_s.downcase
83
+ sort_key(value)
71
84
  end
72
85
  end
73
86
 
87
+ def sort_key(string)
88
+ string = string.to_s
89
+
90
+ collator = self.collator
91
+ return collator.collation_key(string) unless collator.nil?
92
+
93
+ folded = string.downcase(:fold)
94
+ key = [folded.unicode_normalize(:nfd).gsub(/\p{Mn}/, ''), folded]
95
+ sort_case_sensitively? ? key << string : key
96
+ end
97
+
98
+ def collator
99
+ return unless defined?(ICU::Collation::Collator)
100
+
101
+ lc = renderer.locale.to_s
102
+ cs = sort_case_sensitively?
103
+
104
+ @collators ||= {}
105
+ @collators[[lc, cs]] ||= create_collator(lc, cs)
106
+ end
107
+
108
+ def create_collator(locale, case_sensitive)
109
+ collator = ICU::Collation::Collator.new(locale)
110
+ collator.case_first = :upper_first
111
+
112
+ # Secondary strength compares letters and diacritics
113
+ # but ignores case; tertiary strength considers case too.
114
+ collator.strength = case_sensitive ? :tertiary : :secondary
115
+
116
+ collator
117
+ end
118
+
74
119
  def sort_case_sensitively?
75
120
  return false unless processor && processor.options
76
121
  processor.options[:sort_case_sensitively]
77
122
  end
78
-
79
123
  end
80
-
81
124
  end
82
125
  end
@@ -1,5 +1,5 @@
1
1
  module CiteProc
2
2
  module Ruby
3
- VERSION = '2.1.8'
3
+ VERSION = '2.3.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: citeproc-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.8
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvester Keil
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-04 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: citeproc
@@ -15,34 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: 1.0.9
18
+ version: '2.0'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - "~>"
27
24
  - !ruby/object:Gem::Version
28
- version: '1.0'
29
- - - ">="
30
- - !ruby/object:Gem::Version
31
- version: 1.0.9
25
+ version: '2.0'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: csl
34
28
  requirement: !ruby/object:Gem::Requirement
35
29
  requirements:
36
30
  - - "~>"
37
31
  - !ruby/object:Gem::Version
38
- version: '2.0'
32
+ version: '2.5'
39
33
  type: :runtime
40
34
  prerelease: false
41
35
  version_requirements: !ruby/object:Gem::Requirement
42
36
  requirements:
43
37
  - - "~>"
44
38
  - !ruby/object:Gem::Version
45
- version: '2.0'
39
+ version: '2.5'
46
40
  - !ruby/object:Gem::Dependency
47
41
  name: observer
48
42
  requirement: !ruby/object:Gem::Requirement
@@ -58,24 +52,16 @@ dependencies:
58
52
  - !ruby/object:Gem::Version
59
53
  version: '1.0'
60
54
  description: |
61
- CiteProc-Ruby is a Citation Style Language (CSL) 1.0.1 compatible cite
55
+ CiteProc-Ruby is a Citation Style Language (CSL) 1.0.2 compatible cite
62
56
  processor implementation written in pure Ruby.
63
57
  email:
64
- - http://sylvester.keil.or.at
58
+ - sylvester@keil.or.at
65
59
  executables: []
66
60
  extensions: []
67
61
  extra_rdoc_files: []
68
62
  files:
69
- - ".codeclimate.yml"
70
- - ".document"
71
- - ".github/workflows/ci.yml"
72
- - ".yardopts"
73
- - AGPL
74
63
  - BSDL
75
- - Gemfile
76
64
  - README.md
77
- - Rakefile
78
- - cucumber.yml
79
65
  - lib/citeproc/ruby.rb
80
66
  - lib/citeproc/ruby/engine.rb
81
67
  - lib/citeproc/ruby/format.rb
@@ -102,7 +88,10 @@ files:
102
88
  homepage: https://github.com/inukshuk/citeproc-ruby
103
89
  licenses:
104
90
  - BSD-2-Clause
105
- metadata: {}
91
+ metadata:
92
+ source_code_uri: https://github.com/inukshuk/citeproc-ruby
93
+ bug_tracker_uri: https://github.com/inukshuk/citeproc-ruby/issues
94
+ rubygems_mfa_required: 'true'
106
95
  rdoc_options: []
107
96
  require_paths:
108
97
  - lib
@@ -110,14 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
99
  requirements:
111
100
  - - ">="
112
101
  - !ruby/object:Gem::Version
113
- version: 1.9.3
102
+ version: '3.1'
114
103
  required_rubygems_version: !ruby/object:Gem::Requirement
115
104
  requirements:
116
105
  - - ">="
117
106
  - !ruby/object:Gem::Version
118
107
  version: '0'
119
108
  requirements: []
120
- rubygems_version: 3.7.2
109
+ rubygems_version: 4.0.22
121
110
  specification_version: 4
122
111
  summary: A Citation Style Language (CSL) cite processor
123
112
  test_files: []
data/.codeclimate.yml DELETED
@@ -1,17 +0,0 @@
1
- ---
2
- engines:
3
- duplication:
4
- enabled: true
5
- config:
6
- languages:
7
- - ruby
8
- fixme:
9
- enabled: true
10
- rubocop:
11
- enabled: true
12
- ratings:
13
- paths:
14
- - "**.rb"
15
- exclude_paths:
16
- - features/
17
- - spec/
data/.document DELETED
@@ -1,4 +0,0 @@
1
- lib/**/*.rb
2
- -
3
- AGPL
4
- README.md
@@ -1,33 +0,0 @@
1
- name: CI
2
- on:
3
- push:
4
- branches:
5
- - master
6
- pull_request:
7
- branches:
8
- - master
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
- env:
13
- BUNDLE_WITHOUT: debug:extra
14
- strategy:
15
- matrix:
16
- ruby-version:
17
- - '3.4'
18
- - '3.3'
19
- - '3.2'
20
- - '3.1'
21
- steps:
22
- - uses: actions/checkout@v4
23
- - uses: ruby/setup-ruby@v1
24
- with:
25
- ruby-version: ${{ matrix.ruby-version }}
26
- bundler-cache: true
27
- - run: bundle exec rake
28
- - name: Upload coverage results
29
- if: matrix.ruby-version == '3.3'
30
- continue-on-error: true
31
- uses: coverallsapp/github-action@v2
32
- with:
33
- github-token: ${{ github.token }}
data/.yardopts DELETED
@@ -1,2 +0,0 @@
1
- --no-private
2
- --document