csl 1.0.0.pre5 → 1.0.0.pre6

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.
@@ -0,0 +1,130 @@
1
+ module CSL
2
+ class Locale
3
+
4
+ # Ordinalizes the passed-in number using either the ordinal or
5
+ # long-ordinal forms defined by the locale. If a long-ordinal form is
6
+ # requested but not available, the regular ordinal will be returned
7
+ # instead.
8
+ #
9
+ # @example
10
+ # Locale.load('en').ordinalize(13)
11
+ # #-> "13th"
12
+ #
13
+ # de = Locale.load('de')
14
+ # de.ordinalize(13)
15
+ # #-> "13."
16
+ #
17
+ # de.ordinalize(3, :form => :long, :gender => :feminine)
18
+ # #-> "dritte"
19
+ #
20
+ # @note
21
+ # For CSL 1.0 (and older) locales that do not define an "ordinal-00"
22
+ # term the algorithm specified by CSL 1.0 is used; otherwise uses the
23
+ # CSL 1.0.1 algorithm with improved support for languages other than
24
+ # English.
25
+ #
26
+ # @param number [#to_i] the number to ordinalize
27
+ # @param options [Hash] formatting options
28
+ #
29
+ # @option options [:short,:long] :form (:short) which ordinals form to use
30
+ # @option options [:feminine,:masculine,:neutral] :gender (:neutral)
31
+ # which ordinals gender-form to use
32
+ #
33
+ # @raise [ArgumentError] if number cannot be converted to an integer
34
+ #
35
+ # @return [String] the ordinal for the passed-in number
36
+ def ordinalize(number, options = {})
37
+ raise ArgumentError, "unable to ordinalize #{number}; integer expected" unless
38
+ number.respond_to?(:to_i)
39
+
40
+ number, query = number.to_i, ordinalize_query_for(options)
41
+
42
+ key = query[:name]
43
+
44
+ # Try to match long-ordinals first
45
+ if key.start_with?('l')
46
+ query[:name] = key % number.abs
47
+ ordinal = terms[query]
48
+
49
+ if ordinal.nil?
50
+ key = 'ordinal-%02d'
51
+ else
52
+ return ordinal.to_s(options)
53
+ end
54
+ end
55
+
56
+ # CSL 1.0 (legacy algorithm)
57
+ return legacy_ordinalize(number) if legacy?
58
+
59
+ #
60
+ # CSL 1.0.1
61
+ #
62
+
63
+ # Calculate initial modulus
64
+ mod = 10 ** Math.log10([number.abs, 1].max).to_i
65
+
66
+ # Try to find direct match first
67
+ query.merge! :name => key % number.abs
68
+ ordinal = terms[query]
69
+
70
+ # Try to match modulus of number, dividing mod by 10 at each
71
+ # iteration until a match is found
72
+ while ordinal.nil? && mod > 1
73
+ query.merge! :name => key % (number.abs % mod)
74
+ ordinal = terms.lookup_modulo(query, mod)
75
+
76
+ mod = mod / 10
77
+ end
78
+
79
+ # If we have not found a match at this point, we try to match
80
+ # the default ordinal instead
81
+ if ordinal.nil?
82
+ query[:name] = 'ordinal'
83
+ ordinal = terms[query]
84
+
85
+ if ordinal.nil? && query.key?(:'gender-form')
86
+ query.delete(:'gender-form')
87
+ ordinal = terms[query]
88
+ end
89
+ end
90
+
91
+ if ordinal.nil?
92
+ number.to_s
93
+ else
94
+ [number, ordinal.to_s(options)].join
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # @return [Hash] a valid ordinalize query; the name attribute is a format string
101
+ def ordinalize_query_for(options)
102
+ q = { :name => 'ordinal-%02d' }
103
+
104
+ unless options.nil?
105
+ if options.key?(:form) && options[:form].to_s =~ /^long(-ordinal)?$/i
106
+ q[:name] = 'long-ordinal-%02d'
107
+ end
108
+
109
+ gender = (options[:'gender-form'] || options[:gender]).to_s
110
+ unless gender.empty? || gender =~ /^n/i
111
+ q[:'gender-form'] = (gender =~ /^m/i) ? 'masculine' : 'feminine'
112
+ end
113
+ end
114
+
115
+ q
116
+ end
117
+
118
+ def legacy_ordinalize(number)
119
+ case
120
+ when (11..13).include?(number.abs % 100)
121
+ [number, terms['ordinal-04']].join
122
+ when (1..3).include?(number.abs % 10)
123
+ [number, terms['ordinal-%02d' % (number.abs % 10)]].join
124
+ else
125
+ [number, terms['ordinal-04']].join
126
+ end
127
+ end
128
+
129
+ end
130
+ end
@@ -1,91 +1,128 @@
1
1
  module CSL
2
2
  class Locale
3
-
3
+
4
4
  class Terms < Node
5
5
  attr_children :term
6
-
6
+
7
7
  alias terms term
8
8
  def_delegators :terms, :size, :length
9
-
9
+
10
10
  undef_method :[]=
11
-
11
+
12
12
  def initialize(attributes = {})
13
- super(attributes)
13
+ super(attributes)
14
14
  @registry, children[:term] = Hash.new { |h,k| h[k] = [] }, []
15
15
 
16
16
  yield self if block_given?
17
17
  end
18
18
 
19
19
  alias each each_child
20
-
20
+
21
21
  def lookup(query)
22
22
  query = { :name => query } unless query.is_a?(Hash)
23
-
23
+
24
24
  terms = if query[:name].is_a?(Regexp)
25
25
  registry.select { |name, _| name =~ query[:name] }.flatten(1)
26
26
  else
27
27
  registry[query[:name].to_s]
28
28
  end
29
29
 
30
- terms.detect { |t| t.exact_match?(query) }
30
+ terms.detect { |t| t.match?(query) }
31
31
  end
32
-
32
+
33
33
  alias [] lookup
34
+
35
+ def lookup_modulo(query, divisor)
36
+ term = lookup(query)
37
+ return if term.nil? || !term.match_modulo?(divisor)
38
+ term
39
+ end
34
40
 
35
41
  private
36
-
42
+
37
43
  # @!attribute [r] registry
38
44
  # @return [Hash] a private registry to map term names to the respective
39
45
  # term objects for quick term look-up
40
46
  attr_reader :registry
41
-
47
+
42
48
  def added_child(term)
43
49
  raise ValidationError, "failed to register term #{term.inspect}: name attribute missing" unless
44
50
  term.attribute?(:name)
45
-
51
+
46
52
  registry[term[:name]].push(term)
47
53
  term
48
54
  end
49
-
55
+
50
56
  def deleted_child(term)
51
57
  registry[term[:name]].delete(term)
52
58
  end
53
59
  end
54
-
60
+
55
61
  class Term < Node
56
- attr_struct :name, :form, :gender, :'gender-form'
62
+ attr_struct :name, :form, :gender, :'gender-form', :match
57
63
  attr_children :single, :multiple
58
64
 
59
65
  attr_accessor :text
60
66
 
61
67
  def_delegators :attributes, :hash, :eql?, :name, :form, :gender
62
68
 
69
+ # This method returns whether or not the ordinal term matchs the
70
+ # passed-in modulus. This is determined by the ordinal term's match
71
+ # attribute: a value of '2-digits' matches a divisor of 100, '1-digit'
72
+ # matches a divisor of 10 and 'whole-number' matches a divisor of 1.
73
+ #
74
+ # If the term is no ordinal term, this methods always returns false.
75
+ #
76
+ # @return [Boolean] whether or not the ordinal term matches the
77
+ # passed-in divisor.
78
+ def match_modulo?(divisor)
79
+ return false unless ordinal?
80
+
81
+ case attributes.match
82
+ when '2-digits'
83
+ divisor.to_i == 100
84
+ when '1-digit'
85
+ divisor.to_i == 10
86
+ when 'whole-number'
87
+ divisor.to_i == 1
88
+ else
89
+ true
90
+ end
91
+ end
92
+
93
+ alias matches_modulo? match_modulo?
94
+
95
+ # @return [Boolean] whether or not this term is an ordinal term
96
+ def ordinal?
97
+ /^ordinal(-\d\d+)?$/ === attributes.name
98
+ end
99
+
63
100
  def gendered?
64
101
  !attributes.gender.blank?
65
102
  end
66
-
103
+
67
104
  def neutral?
68
105
  !gendered?
69
106
  end
70
-
107
+
71
108
  def textnode?
72
109
  !text.blank?
73
110
  end
74
-
111
+
75
112
  def singularize
76
113
  return text if textnode?
77
114
  children.single.to_s
78
115
  end
79
116
 
80
117
  alias singular singularize
81
-
118
+
82
119
  def pluralize
83
120
  return text if textnode?
84
121
  children.multiple.to_s
85
- end
122
+ end
123
+
124
+ alias plural pluralize
86
125
 
87
- alias plural pluralize
88
-
89
126
  # @!method masculine?
90
127
  # @return [Boolean] whether or not the term is masculine
91
128
 
@@ -103,7 +140,7 @@ module CSL
103
140
  define_method("#{name}?") do
104
141
  attributes.gender.to_s == name
105
142
  end
106
-
143
+
107
144
  define_method("#{name}!") do
108
145
  return nil if attributes.gender.to_s == name
109
146
  attributes.gender = name
@@ -139,21 +176,21 @@ module CSL
139
176
  end
140
177
  end
141
178
  end
142
-
179
+
143
180
  class Single < TextNode; end
144
181
  class Multiple < TextNode; end
145
-
182
+
146
183
  private
147
-
184
+
148
185
  def pluralize?(options)
149
186
  return false if options.nil?
150
-
187
+
151
188
  case
152
189
  when options.key?(:plural) || options.key?('plural')
153
190
  options[:plural] || options['plural']
154
191
  when options.key?(:number) || options.key?('number')
155
192
  key = options[:number] || options['number']
156
-
193
+
157
194
  if key.is_a?(Fixnum) || key.to_s =~ /^[+-]?\d+$/
158
195
  key.to_i > 1
159
196
  else
@@ -163,9 +200,9 @@ module CSL
163
200
  false
164
201
  end
165
202
  end
166
-
203
+
167
204
  end
168
-
205
+
169
206
  TextNode.types << Term
170
207
  end
171
208
  end
data/lib/csl/node.rb CHANGED
@@ -68,6 +68,28 @@ module CSL
68
68
 
69
69
  private
70
70
 
71
+ def has_language
72
+ attr_accessor :language
73
+
74
+ define_method :has_language? do
75
+ !language.nil?
76
+ end
77
+
78
+ public :language, :language=, :has_language?
79
+
80
+ alias_method :original_attribute_assignments, :attribute_assignments
81
+
82
+ define_method :attribute_assignments do
83
+ if has_language?
84
+ original_attribute_assignments.unshift('xml:lang="%s"' % language)
85
+ else
86
+ original_attribute_assignments
87
+ end
88
+ end
89
+
90
+ private :original_attribute_assignments, :attribute_assignments
91
+ end
92
+
71
93
  def attr_defaults(attributes)
72
94
  @default_attributes = attributes
73
95
  end
@@ -193,6 +215,10 @@ module CSL
193
215
  !attributes.empty?
194
216
  end
195
217
 
218
+ def has_language?
219
+ false
220
+ end
221
+
196
222
  def textnode?
197
223
  false
198
224
  end
@@ -208,7 +234,7 @@ module CSL
208
234
 
209
235
  # Tests whether or not the Name matches the passed-in node name and
210
236
  # attribute conditions; if a Hash is passed as a single argument,
211
- # it is taken as the conditions parameter (the name parameter is
237
+ # it is taken as the conditions parameter (the name parameter
212
238
  # automatically matches in this case).
213
239
  #
214
240
  # Whether or not the arguments match the node is determined as
@@ -225,6 +251,7 @@ module CSL
225
251
  #
226
252
  # @see #exact_match?
227
253
  #
254
+ # If the optional
228
255
  # @param name [String,Regexp] must match the nodename
229
256
  # @param conditions [Hash] the conditions
230
257
  #
@@ -276,6 +303,21 @@ module CSL
276
303
  end
277
304
  alias matches_exactly? exact_match?
278
305
 
306
+ # @option filter [Array] a list of attribute names
307
+ # @return [Hash] the node's attributes matching the filter
308
+ def attributes_for(*filter)
309
+ filter.flatten!
310
+
311
+ Hash[map { |name, value|
312
+ !value.nil? && filter.include?(name) ? [name, value.to_s] : nil
313
+ }.compact]
314
+ end
315
+
316
+ # @return [Hash] the node's formatting options
317
+ def formatting_options
318
+ attributes_for Schema.attr(:formatting)
319
+ end
320
+
279
321
  def <=>(other)
280
322
  [nodename, attributes, children] <=> [other.nodename, other.attributes, other.children]
281
323
  rescue
@@ -311,7 +353,7 @@ module CSL
311
353
 
312
354
  def attribute_assignments
313
355
  each_pair.map { |name, value|
314
- value.nil? ? nil: [name, value.to_s.inspect].join('=')
356
+ value.nil? ? nil : [name, value.to_s.inspect].join('=')
315
357
  }.compact
316
358
  end
317
359
 
data/lib/csl/schema.rb CHANGED
@@ -86,6 +86,10 @@ module CSL
86
86
  })
87
87
 
88
88
  @attributes.each_value { |v| v.map!(&:to_sym).freeze }
89
+
90
+ @attributes[:formatting] = [:'text-case'].concat(
91
+ @attributes.values_at(:affixes, :quotes, :font).flatten)
92
+
89
93
  @attributes.freeze
90
94
 
91
95
  @file = File.expand_path('../../../vendor/schema/csl.rng', __FILE__)
@@ -4,7 +4,8 @@ module CSL
4
4
  class Number < Node
5
5
  attr_struct :variable, :form, :'text-case',
6
6
  *Schema.attr(:affixes, :display, :font)
7
-
7
+
8
+ has_no_children
8
9
 
9
10
  # @return [Boolean] whether or not the number's format is set to
10
11
  # :numeric; also returns true if the number's form attribute is not
@@ -1,10 +1,12 @@
1
1
  module CSL
2
2
  class Style
3
-
3
+
4
4
  class Text < Node
5
- attr_struct :variable, :macro, :term, :form, :plural, :'text-case',
6
- :value, *Schema.attr(:affixes, :display, :font, :quotes, :periods)
5
+ attr_struct :variable, :macro, :term, :form, :plural, :value,
6
+ *Schema.attr(:formatting, :display, :periods)
7
+
8
+ has_no_children
7
9
  end
8
-
10
+
9
11
  end
10
12
  end