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.
- 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
|