csl 1.0.0.pre5 → 1.0.0.pre6
Sign up to get free protection for your applications and to get access to all the features.
- data/csl.gemspec +3 -0
- data/features/locales/ordinalize.feature +288 -399
- data/lib/csl.rb +3 -0
- data/lib/csl/info.rb +31 -18
- data/lib/csl/locale.rb +64 -168
- data/lib/csl/locale/ordinalize.rb +130 -0
- data/lib/csl/locale/term.rb +67 -30
- data/lib/csl/node.rb +44 -2
- data/lib/csl/schema.rb +4 -0
- data/lib/csl/style/number.rb +2 -1
- data/lib/csl/style/text.rb +6 -4
- data/lib/csl/version.rb +1 -1
- data/spec/csl/info_spec.rb +7 -1
- data/spec/csl/locale/term_spec.rb +1 -1
- data/spec/csl/node_spec.rb +40 -0
- metadata +23 -8
data/lib/csl.rb
CHANGED
@@ -6,6 +6,8 @@ require 'singleton'
|
|
6
6
|
require 'set'
|
7
7
|
require 'time'
|
8
8
|
|
9
|
+
require 'namae'
|
10
|
+
|
9
11
|
require 'csl/version'
|
10
12
|
|
11
13
|
require 'csl/compatibility'
|
@@ -23,6 +25,7 @@ require 'csl/node'
|
|
23
25
|
require 'csl/info'
|
24
26
|
|
25
27
|
require 'csl/locale'
|
28
|
+
require 'csl/locale/ordinalize'
|
26
29
|
require 'csl/locale/date'
|
27
30
|
require 'csl/locale/term'
|
28
31
|
require 'csl/locale/style_options'
|
data/lib/csl/info.rb
CHANGED
@@ -105,16 +105,16 @@ module CSL
|
|
105
105
|
# @return [self]
|
106
106
|
def update!(timestamp = Time.now)
|
107
107
|
ts = timestamp.respond_to?(:xmlschema) ? timestamp.xmlschema : timestamp.to_s
|
108
|
-
|
108
|
+
|
109
109
|
if has_updated?
|
110
110
|
updated = Updated.new { |u| u.text = ts }
|
111
111
|
else
|
112
112
|
updated.text = ts
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
self
|
116
116
|
end
|
117
|
-
|
117
|
+
|
118
118
|
# @return [Time,nil] when the info node's parent was published
|
119
119
|
def published_at
|
120
120
|
return unless has_published?
|
@@ -125,30 +125,30 @@ module CSL
|
|
125
125
|
# @return [self]
|
126
126
|
def publish!(timestamp = Time.now)
|
127
127
|
ts = timestamp.respond_to?(:xmlschema) ? timestamp.xmlschema : timestamp.to_s
|
128
|
-
|
128
|
+
|
129
129
|
if has_published?
|
130
130
|
published = Published.new { |u| u.text = ts }
|
131
131
|
else
|
132
132
|
published.text = ts
|
133
133
|
end
|
134
|
-
|
134
|
+
|
135
135
|
self
|
136
136
|
end
|
137
|
-
|
137
|
+
|
138
138
|
# @return [Symbol] the parent style's citation format
|
139
139
|
def citation_format
|
140
140
|
return unless has_categories?
|
141
|
-
|
141
|
+
|
142
142
|
cat = categories.detect { |c| c.attribute? :'citation-format' }
|
143
143
|
return if cat.nil?
|
144
|
-
|
144
|
+
|
145
145
|
cat[:'citation-format'].to_sym
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
def citation_format=(new_format)
|
149
|
-
cat = categories.detect { |c| c.attribute? :'citation-format' }
|
149
|
+
cat = categories.detect { |c| c.attribute? :'citation-format' }
|
150
150
|
cat = add_child Info::Category.new if cat.nil?
|
151
|
-
|
151
|
+
|
152
152
|
cat[:'citation-format'] = new_format.to_s
|
153
153
|
end
|
154
154
|
|
@@ -158,20 +158,22 @@ module CSL
|
|
158
158
|
|
159
159
|
class Contributor < Node
|
160
160
|
attr_children :name, :email, :uri
|
161
|
+
def_delegators :name, *Namae::Name.members
|
161
162
|
end
|
162
163
|
|
163
164
|
class Author < Node
|
164
165
|
attr_children :name, :email, :uri
|
166
|
+
def_delegators :name, *Namae::Name.members
|
165
167
|
end
|
166
168
|
|
167
169
|
class Translator < Node
|
168
170
|
attr_children :name, :email, :uri
|
171
|
+
def_delegators :name, *Namae::Name.members
|
169
172
|
end
|
170
173
|
|
171
174
|
class Link < Node
|
175
|
+
has_language
|
172
176
|
attr_struct :href, :rel
|
173
|
-
|
174
|
-
# TODO xml:lang
|
175
177
|
end
|
176
178
|
|
177
179
|
class DependentStyle < TextNode
|
@@ -187,6 +189,18 @@ module CSL
|
|
187
189
|
end
|
188
190
|
|
189
191
|
class Name < TextNode
|
192
|
+
|
193
|
+
def_delegators :namae, *Namae::Name.members
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def namae
|
198
|
+
@namae || namae!
|
199
|
+
end
|
200
|
+
|
201
|
+
def namae!
|
202
|
+
@namae = Namae::Name.parse to_s
|
203
|
+
end
|
190
204
|
end
|
191
205
|
|
192
206
|
class Email < TextNode
|
@@ -196,24 +210,23 @@ module CSL
|
|
196
210
|
end
|
197
211
|
|
198
212
|
class Title < TextNode
|
199
|
-
|
213
|
+
has_language
|
200
214
|
end
|
201
215
|
|
202
216
|
class TitleShort < TextNode
|
203
|
-
|
217
|
+
has_language
|
204
218
|
end
|
205
219
|
|
206
220
|
class Summary < TextNode
|
207
|
-
|
221
|
+
has_language
|
208
222
|
end
|
209
223
|
|
210
224
|
class Rights < TextNode
|
225
|
+
has_language
|
211
226
|
attr_struct :license
|
212
|
-
# TODO xml:lang
|
213
227
|
end
|
214
228
|
|
215
229
|
class Updated < TextNode
|
216
|
-
|
217
230
|
def to_time
|
218
231
|
return if empty?
|
219
232
|
Time.parse(to_s)
|
data/lib/csl/locale.rb
CHANGED
@@ -5,17 +5,17 @@ module CSL
|
|
5
5
|
#
|
6
6
|
class Locale < Node
|
7
7
|
types << CSL::Info
|
8
|
-
|
8
|
+
|
9
9
|
include Comparable
|
10
10
|
|
11
11
|
@default = 'en-US'.freeze
|
12
12
|
|
13
13
|
@root = File.expand_path('../../../vendor/locales', __FILE__).freeze
|
14
|
-
|
14
|
+
|
15
15
|
@extension = '.xml'.freeze
|
16
16
|
@prefix = 'locales-'.freeze
|
17
|
-
|
18
|
-
|
17
|
+
|
18
|
+
|
19
19
|
# Default languages/regions.
|
20
20
|
# Auto-detection is based on these lists.
|
21
21
|
@regions = Hash[*%w{
|
@@ -28,38 +28,40 @@ module CSL
|
|
28
28
|
@languages = @regions.invert.merge(Hash[*%w{
|
29
29
|
AT de BR pt CA en CH de GB en
|
30
30
|
}.map(&:to_sym)]).freeze
|
31
|
-
|
32
|
-
|
31
|
+
|
32
|
+
|
33
33
|
class << self
|
34
34
|
include Loader
|
35
|
-
|
35
|
+
|
36
36
|
attr_accessor :default
|
37
37
|
attr_reader :languages, :regions
|
38
38
|
|
39
39
|
def parse(data)
|
40
40
|
node = CSL.parse!(data, self)
|
41
|
-
|
41
|
+
|
42
42
|
raise ParseError, "root node is not a locale: #{node.inspect}" unless
|
43
43
|
node.is_a?(self)
|
44
|
-
|
44
|
+
|
45
45
|
node
|
46
|
-
end
|
46
|
+
end
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
attr_defaults :version => Schema.version, :xmlns => Schema.namespace
|
50
50
|
attr_struct :xmlns, :version
|
51
51
|
|
52
52
|
attr_children :'style-options', :info, :date, :terms
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
|
54
|
+
has_language
|
55
|
+
|
56
|
+
attr_accessor :region
|
57
|
+
|
56
58
|
alias_child :metadata, :info
|
57
59
|
alias_child :dates, :date
|
58
60
|
alias_child :options, :style_options
|
59
61
|
|
60
62
|
private :attributes
|
61
63
|
undef_method :[]=
|
62
|
-
|
64
|
+
|
63
65
|
# call-seq:
|
64
66
|
# Locale.new -> default
|
65
67
|
# Locale.new('en') -> American English
|
@@ -78,10 +80,10 @@ module CSL
|
|
78
80
|
when 1
|
79
81
|
if arguments[0].is_a?(Hash)
|
80
82
|
arguments[0] = arguments[0].symbolize_keys
|
81
|
-
|
83
|
+
|
82
84
|
locale = arguments[0].delete(:lang) ||
|
83
85
|
arguments[0].delete(:'xml:lang') || Locale.default
|
84
|
-
|
86
|
+
|
85
87
|
attributes, options = arguments
|
86
88
|
else
|
87
89
|
attributes, locale, options = {}, arguments
|
@@ -91,15 +93,15 @@ module CSL
|
|
91
93
|
else
|
92
94
|
raise ArgumentError, "wrong number of arguments (#{arguments.length} for 0..2)"
|
93
95
|
end
|
94
|
-
|
96
|
+
|
95
97
|
super(attributes, &nil)
|
96
|
-
|
98
|
+
|
97
99
|
set(locale) unless locale.nil?
|
98
|
-
|
100
|
+
|
99
101
|
unless options.nil?
|
100
102
|
children[:'style-options'] = StyleOptions.new(options)
|
101
103
|
end
|
102
|
-
|
104
|
+
|
103
105
|
yield self if block_given?
|
104
106
|
end
|
105
107
|
|
@@ -107,31 +109,31 @@ module CSL
|
|
107
109
|
# def initialize_copy(other)
|
108
110
|
# @options = other.options.dup
|
109
111
|
# end
|
110
|
-
|
111
|
-
|
112
|
+
|
113
|
+
|
112
114
|
def added_to(node)
|
113
115
|
raise ValidationError, "not allowed to add locale to #{node.nodename}" unless
|
114
116
|
node.nodename == 'style'
|
115
117
|
end
|
116
|
-
|
117
|
-
|
118
|
+
|
119
|
+
|
118
120
|
def version
|
119
121
|
attributes[:version]
|
120
122
|
end
|
121
|
-
|
123
|
+
|
122
124
|
def version=(version)
|
123
125
|
raise ArgumentError, "failed to set version to #{version}" unless
|
124
126
|
version.respond_to?(:to_s)
|
125
|
-
|
127
|
+
|
126
128
|
version = version.to_s.strip
|
127
|
-
|
129
|
+
|
128
130
|
raise ArgumentError, "failed to set version to #{version}: not a version string" unless
|
129
131
|
version =~ /^\d[\d\.]+$/
|
130
132
|
|
131
133
|
if version > Schema.version
|
132
134
|
warn "setting version to #{version}; latest supported version is #{Schema.version}"
|
133
135
|
end
|
134
|
-
|
136
|
+
|
135
137
|
attributes[:version] = version
|
136
138
|
end
|
137
139
|
|
@@ -139,24 +141,27 @@ module CSL
|
|
139
141
|
def legacy?
|
140
142
|
version < Schema.version
|
141
143
|
end
|
142
|
-
|
143
|
-
#
|
144
|
-
# locale.set('en')
|
145
|
-
# locale.set('de-AT')
|
146
|
-
# locale.set('-DE')
|
144
|
+
|
145
|
+
# @example
|
146
|
+
# locale.set('en') #-> sets language to :en, region to :US
|
147
|
+
# locale.set('de-AT') #-> sets language to :de, region to :AT
|
148
|
+
# locale.set('-DE') #-> sets langauge to :de, region to :DE
|
147
149
|
#
|
148
150
|
# Sets language and region according to the passed-in locale string. If
|
149
151
|
# the region part is not defined by the string, this method will set the
|
150
152
|
# region to the default region for the given language.
|
151
153
|
#
|
152
|
-
#
|
153
|
-
|
154
|
-
|
154
|
+
# @raise [ArgumentError] if the argument is no valid locale string.
|
155
|
+
# A valid locale string is based on the syntax of IETF language tags;
|
156
|
+
# it consists of either a language or region tag (or both), separated
|
157
|
+
# by a hyphen.
|
158
|
+
#
|
159
|
+
# @return [self]
|
155
160
|
def set(locale)
|
156
161
|
language, region = locale.to_s.scan(/([a-z]{2})?(?:-([A-Z]{2}))?/)[0].map do |tag|
|
157
162
|
tag.respond_to?(:to_sym) ? tag.to_sym : nil
|
158
163
|
end
|
159
|
-
|
164
|
+
|
160
165
|
case
|
161
166
|
when language && region
|
162
167
|
@language, @region = language, region
|
@@ -167,26 +172,27 @@ module CSL
|
|
167
172
|
else
|
168
173
|
raise ArgumentError, "not a valid locale string: #{locale.inspect}"
|
169
174
|
end
|
170
|
-
|
175
|
+
|
171
176
|
self
|
172
177
|
end
|
173
|
-
|
178
|
+
|
174
179
|
# Sets the locale's language and region to nil.
|
180
|
+
# @return [self]
|
175
181
|
def clear
|
176
182
|
@language, @region = nil
|
177
183
|
self
|
178
184
|
end
|
179
|
-
|
185
|
+
|
180
186
|
def translate(*arguments)
|
181
187
|
raise 'not implemented'
|
182
188
|
end
|
183
189
|
|
184
190
|
alias _ translate
|
185
191
|
alias t translate
|
186
|
-
|
187
|
-
#
|
188
|
-
# locale.each_term { |term| block }
|
189
|
-
# locale.each_term
|
192
|
+
|
193
|
+
# @example
|
194
|
+
# locale.each_term { |term| block } #-> locale
|
195
|
+
# locale.each_term #-> enumerator
|
190
196
|
#
|
191
197
|
# Calls block once for each term defined by the locale. If no block is
|
192
198
|
# given, an enumerator is returned instead.
|
@@ -198,10 +204,10 @@ module CSL
|
|
198
204
|
enum_for :each_term
|
199
205
|
end
|
200
206
|
end
|
201
|
-
|
202
|
-
#
|
203
|
-
# locale.each_date { |date_format| block }
|
204
|
-
# locale.each_date
|
207
|
+
|
208
|
+
# @example
|
209
|
+
# locale.each_date { |date_format| block } #-> locale
|
210
|
+
# locale.each_date #-> enumerator
|
205
211
|
#
|
206
212
|
# Calls block once for each date format defined by the locale. If no
|
207
213
|
# block is given, an enumerator is returned instead.
|
@@ -212,12 +218,12 @@ module CSL
|
|
212
218
|
enum_for :each_date
|
213
219
|
end
|
214
220
|
end
|
215
|
-
|
221
|
+
|
216
222
|
# @returns [Boolean] whether or not the Locale is the default locale
|
217
223
|
def default?
|
218
224
|
to_s == Locale.default
|
219
225
|
end
|
220
|
-
|
226
|
+
|
221
227
|
# @return [Boolean] whehter or not the Locale's region is the default
|
222
228
|
# region for its language
|
223
229
|
def default_region?
|
@@ -230,92 +236,10 @@ module CSL
|
|
230
236
|
language && language == Locale.languages[region]
|
231
237
|
end
|
232
238
|
|
233
|
-
# Ordinalizes the passed-in number using either the ordinal or
|
234
|
-
# long-ordinal forms defined by the locale. If a long-ordinal form is
|
235
|
-
# requested but not available, the regular ordinal will be returned
|
236
|
-
# instead.
|
237
|
-
#
|
238
|
-
# @example
|
239
|
-
# Locale.load('en').ordinalize(13)
|
240
|
-
# #-> "13th"
|
241
|
-
#
|
242
|
-
# de = Locale.load('de')
|
243
|
-
# de.ordinalize(13)
|
244
|
-
# #-> "13."
|
245
|
-
#
|
246
|
-
# de.ordinalize(3, :form => :long, :gender => :feminine)
|
247
|
-
# #-> "dritte"
|
248
|
-
#
|
249
|
-
# @note
|
250
|
-
# For CSL 1.0 (and older) locales that do not define an "ordinal-00"
|
251
|
-
# term the algorithm specified by CSL 1.0 is used; otherwise uses the
|
252
|
-
# CSL 1.0.1 algorithm with improved support for languages other than
|
253
|
-
# English.
|
254
|
-
#
|
255
|
-
# @param number [#to_i] the number to ordinalize
|
256
|
-
# @param options [Hash] formatting options
|
257
|
-
#
|
258
|
-
# @option options [:short,:long] :form (:short) which ordinals form to use
|
259
|
-
# @option options [:feminine,:masculine,:neutral] :gender (:neutral)
|
260
|
-
# which ordinals gender-form to use
|
261
|
-
#
|
262
|
-
# @raise [ArgumentError] if number cannot be converted to an integer
|
263
|
-
#
|
264
|
-
# @return [String] the ordinal for the passed-in number
|
265
|
-
def ordinalize(number, options = {})
|
266
|
-
raise ArgumentError, "unable to ordinalize #{number}; integer expected" unless
|
267
|
-
number.respond_to?(:to_i)
|
268
|
-
|
269
|
-
number, query = number.to_i, ordinalize_query_for(options)
|
270
|
-
|
271
|
-
key = query[:name]
|
272
|
-
|
273
|
-
# try to match long-ordinals first
|
274
|
-
if key.start_with?('l')
|
275
|
-
query[:name] = key % number.abs
|
276
|
-
ordinal = terms[query]
|
277
|
-
|
278
|
-
if ordinal.nil?
|
279
|
-
key = 'ordinal-%02d'
|
280
|
-
else
|
281
|
-
return ordinal.to_s(options)
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
# CSL 1.0
|
286
|
-
if legacy? || terms['ordinal-00'].nil?
|
287
|
-
return legacy_ordinalize(number)
|
288
|
-
end
|
289
|
-
|
290
|
-
# CSL 1.0.1
|
291
|
-
# 1. try to find exact match
|
292
|
-
# 2. if no match is found, try to match modulus of number,
|
293
|
-
# dividing mod by 10 at each iteration
|
294
|
-
# 3. repeat until a match is found or mod reaches 0
|
295
|
-
|
296
|
-
mod = 10 ** Math.log10([number.abs, 1].max).to_i
|
297
|
-
|
298
|
-
query[:name] = key % number.abs
|
299
|
-
ordinal = terms[query]
|
300
|
-
|
301
|
-
while ordinal.nil? && mod > 0
|
302
|
-
query[:name] = key % (number.abs % mod)
|
303
|
-
ordinal = terms[query]
|
304
|
-
mod = mod / 10
|
305
|
-
end
|
306
|
-
|
307
|
-
if ordinal.nil? && query.key?(:'gender-form')
|
308
|
-
query.delete(:'gender-form')
|
309
|
-
ordinal = terms[query]
|
310
|
-
end
|
311
|
-
|
312
|
-
[number, ordinal.to_s(options)].join
|
313
|
-
end
|
314
|
-
|
315
239
|
def validate
|
316
240
|
Schema.validate self
|
317
241
|
end
|
318
|
-
|
242
|
+
|
319
243
|
def valid?
|
320
244
|
validate.empty?
|
321
245
|
end
|
@@ -356,14 +280,14 @@ module CSL
|
|
356
280
|
def to_s
|
357
281
|
[language, region].compact.join('-')
|
358
282
|
end
|
359
|
-
|
283
|
+
|
360
284
|
# @return [String] a string representation of the Locale
|
361
285
|
def inspect
|
362
286
|
"#<#{self.class.name} #{to_s}>"
|
363
287
|
end
|
364
|
-
|
288
|
+
|
365
289
|
private
|
366
|
-
|
290
|
+
|
367
291
|
def attribute_assignments
|
368
292
|
if root?
|
369
293
|
super.push('xml:lang="%s"' % to_s)
|
@@ -371,39 +295,11 @@ module CSL
|
|
371
295
|
'xml:lang="%s"' % to_s
|
372
296
|
end
|
373
297
|
end
|
374
|
-
|
298
|
+
|
375
299
|
def preamble
|
376
300
|
Schema.preamble.dup
|
377
|
-
end
|
378
|
-
|
379
|
-
# @return [Hash] a valid ordinalize query; the name attribute is a format string
|
380
|
-
def ordinalize_query_for(options)
|
381
|
-
q = { :name => 'ordinal-%02d' }
|
382
|
-
|
383
|
-
unless options.nil?
|
384
|
-
if options.key?(:form) && options[:form].to_s =~ /^long(-ordinal)?$/i
|
385
|
-
q[:name] = 'long-ordinal-%02d'
|
386
|
-
end
|
387
|
-
|
388
|
-
gender = (options[:'gender-form'] || options[:gender]).to_s
|
389
|
-
unless gender.empty? || gender =~ /^n/i
|
390
|
-
q[:'gender-form'] = (gender =~ /^m/i) ? 'masculine' : 'feminine'
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
q
|
395
|
-
end
|
396
|
-
|
397
|
-
def legacy_ordinalize(number)
|
398
|
-
case
|
399
|
-
when (11..13).include?(number.abs % 100)
|
400
|
-
[number, terms['ordinal-04']].join
|
401
|
-
when (1..3).include?(number.abs % 10)
|
402
|
-
[number, terms['ordinal-%02d' % (number.abs % 10)]].join
|
403
|
-
else
|
404
|
-
[number, terms['ordinal-04']].join
|
405
|
-
end
|
406
301
|
end
|
302
|
+
|
407
303
|
end
|
408
|
-
|
304
|
+
|
409
305
|
end
|