csl 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +8 -0
- data/.gitmodules +6 -0
- data/.rspec +3 -0
- data/.simplecov +2 -0
- data/.travis.yml +13 -0
- data/.yardopts +2 -0
- data/AGPL +662 -0
- data/BSDL +29 -0
- data/Gemfile +24 -0
- data/Guardfile +14 -0
- data/README.md +39 -0
- data/Rakefile +45 -0
- data/csl.gemspec +36 -0
- data/cucumber.yml +1 -0
- data/features/locales/loading.feature +57 -0
- data/features/locales/ordinalize.feature +861 -0
- data/features/parser/info.feature +27 -0
- data/features/parser/localized_dates.feature +35 -0
- data/features/parser/terms.feature +28 -0
- data/features/step_definitions/locale_steps.rb +34 -0
- data/features/step_definitions/parser_steps.rb +28 -0
- data/features/step_definitions/style_steps.rb +16 -0
- data/features/style/loading.feature +53 -0
- data/features/support/env.rb +8 -0
- data/lib/csl.rb +54 -0
- data/lib/csl/compatibility.rb +19 -0
- data/lib/csl/errors.rb +15 -0
- data/lib/csl/extensions.rb +63 -0
- data/lib/csl/info.rb +40 -0
- data/lib/csl/loader.rb +78 -0
- data/lib/csl/locale.rb +393 -0
- data/lib/csl/locale/date.rb +48 -0
- data/lib/csl/locale/style_options.rb +10 -0
- data/lib/csl/locale/term.rb +185 -0
- data/lib/csl/node.rb +285 -0
- data/lib/csl/parser.rb +92 -0
- data/lib/csl/pretty_printer.rb +33 -0
- data/lib/csl/schema.rb +109 -0
- data/lib/csl/style.rb +53 -0
- data/lib/csl/style/bibliography.rb +15 -0
- data/lib/csl/style/citation.rb +17 -0
- data/lib/csl/style/conditional.rb +11 -0
- data/lib/csl/style/date.rb +16 -0
- data/lib/csl/style/group.rb +9 -0
- data/lib/csl/style/label.rb +14 -0
- data/lib/csl/style/layout.rb +10 -0
- data/lib/csl/style/macro.rb +9 -0
- data/lib/csl/style/names.rb +54 -0
- data/lib/csl/style/number.rb +33 -0
- data/lib/csl/style/sort.rb +21 -0
- data/lib/csl/style/text.rb +10 -0
- data/lib/csl/treelike.rb +442 -0
- data/lib/csl/version.rb +3 -0
- data/spec/csl/info_spec.rb +116 -0
- data/spec/csl/locale/date_spec.rb +63 -0
- data/spec/csl/locale/style_options_spec.rb +19 -0
- data/spec/csl/locale/term_spec.rb +96 -0
- data/spec/csl/locale_spec.rb +128 -0
- data/spec/csl/node_spec.rb +100 -0
- data/spec/csl/parser_spec.rb +92 -0
- data/spec/csl/schema_spec.rb +70 -0
- data/spec/csl/style/bibliography_spec.rb +7 -0
- data/spec/csl/style/citation_spec.rb +7 -0
- data/spec/csl/style/conditional_spec.rb +7 -0
- data/spec/csl/style/date_spec.rb +11 -0
- data/spec/csl/style/group_spec.rb +7 -0
- data/spec/csl/style/label_spec.rb +7 -0
- data/spec/csl/style/layout_spec.rb +7 -0
- data/spec/csl/style/macro_spec.rb +7 -0
- data/spec/csl/style/names_spec.rb +23 -0
- data/spec/csl/style/number_spec.rb +84 -0
- data/spec/csl/style/text_spec.rb +7 -0
- data/spec/csl/style_spec.rb +19 -0
- data/spec/csl/treelike_spec.rb +151 -0
- data/spec/spec_helper.rb +30 -0
- metadata +192 -0
data/lib/csl/locale.rb
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
module CSL
|
2
|
+
#
|
3
|
+
# CSL::Locales contain locale specific date formatting options, term
|
4
|
+
# translations, and a number ordinalizer.
|
5
|
+
#
|
6
|
+
class Locale < Node
|
7
|
+
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
@default = 'en-US'.freeze
|
11
|
+
|
12
|
+
@root = File.expand_path('../../../vendor/locales', __FILE__).freeze
|
13
|
+
|
14
|
+
@extension = '.xml'.freeze
|
15
|
+
@prefix = 'locales-'.freeze
|
16
|
+
|
17
|
+
|
18
|
+
# Default languages/regions.
|
19
|
+
# Auto-detection is based on these lists.
|
20
|
+
@regions = Hash[*%w{
|
21
|
+
af ZA ar AR bg BG ca AD cs CZ da DK de DE el GR en US es ES et EE fa IR
|
22
|
+
fr FR he IL hu HU is IS it IT ja JP km KH ko KR mn MN nb NO nl NL nn NO
|
23
|
+
pl PL pt PT ro RO ru RU sk SK sl SI sr RS sv SE th TH tr TR uk UA vi VN
|
24
|
+
zh CN zh TW
|
25
|
+
}.map(&:to_sym)].freeze
|
26
|
+
|
27
|
+
@languages = @regions.invert.merge(Hash[*%w{
|
28
|
+
AT de BR pt CA en CH de GB en
|
29
|
+
}.map(&:to_sym)]).freeze
|
30
|
+
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
include Loader
|
35
|
+
|
36
|
+
attr_accessor :default
|
37
|
+
attr_reader :languages, :regions
|
38
|
+
|
39
|
+
def parse(data)
|
40
|
+
node = CSL.parse!(data)
|
41
|
+
|
42
|
+
raise ParseError, "root node is not a locale: #{node.inspect}" unless
|
43
|
+
node.is_a?(self)
|
44
|
+
|
45
|
+
node
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_defaults :version => Schema.version, :xmlns => Schema.namespace
|
51
|
+
|
52
|
+
attr_children :'style-options', :info, :date, :terms
|
53
|
+
|
54
|
+
attr_accessor :language, :region
|
55
|
+
|
56
|
+
alias metadata info
|
57
|
+
alias dates date
|
58
|
+
alias options style_options
|
59
|
+
|
60
|
+
private :attributes
|
61
|
+
undef_method :[]=
|
62
|
+
|
63
|
+
# call-seq:
|
64
|
+
# Locale.new -> default
|
65
|
+
# Locale.new('en') -> American English
|
66
|
+
# Locale.new('en', :'punctuation-in-quote' => fales) -> with style-options
|
67
|
+
# Locale.new(:lang => 'en-GB', :version => '1.0') -> British English
|
68
|
+
#
|
69
|
+
# Returns a new locale. In the first form, the language/regions is set
|
70
|
+
# to the default language and region. In the second form the
|
71
|
+
# language/region is set by the passed-in IETF tag. The third form
|
72
|
+
# additionally accepts a hash of localize style-options. The fourth form
|
73
|
+
# is the standard node attribute initialize signature.
|
74
|
+
def initialize(*arguments)
|
75
|
+
case arguments.length
|
76
|
+
when 0
|
77
|
+
locale, attributes, options = Locale.default, {}, nil
|
78
|
+
when 1
|
79
|
+
if arguments[0].is_a?(Hash)
|
80
|
+
arguments[0] = arguments[0].symbolize_keys
|
81
|
+
|
82
|
+
locale = arguments[0].delete(:lang) ||
|
83
|
+
arguments[0].delete(:'xml:lang') || Locale.default
|
84
|
+
|
85
|
+
attributes, options = arguments
|
86
|
+
else
|
87
|
+
attributes, locale, options = {}, arguments
|
88
|
+
end
|
89
|
+
when 2
|
90
|
+
attributes, locale, options = {}, *arguments
|
91
|
+
else
|
92
|
+
raise ArgumentError, "wrong number of arguments (#{arguments.length} for 0..2)"
|
93
|
+
end
|
94
|
+
|
95
|
+
super(attributes)
|
96
|
+
|
97
|
+
set(locale) unless locale.nil?
|
98
|
+
|
99
|
+
unless options.nil?
|
100
|
+
children[:'style-options'] = StyleOptions.new(options)
|
101
|
+
end
|
102
|
+
|
103
|
+
yield self if block_given?
|
104
|
+
end
|
105
|
+
|
106
|
+
# TODO
|
107
|
+
# def initialize_copy(other)
|
108
|
+
# @options = other.options.dup
|
109
|
+
# end
|
110
|
+
|
111
|
+
|
112
|
+
def added_to(node)
|
113
|
+
raise ValidationError, "not allowed to add locale to #{node.nodename}" unless
|
114
|
+
node.nodename == 'style'
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def version
|
119
|
+
attributes[:version]
|
120
|
+
end
|
121
|
+
|
122
|
+
def version=(version)
|
123
|
+
raise ArgumentError, "failed to set version to #{version}" unless
|
124
|
+
version.respond_to?(:to_s)
|
125
|
+
|
126
|
+
version = version.to_s.strip
|
127
|
+
|
128
|
+
raise ArgumentError, "failed to set version to #{version}: not a version string" unless
|
129
|
+
version =~ /^\d[\d\.]+$/
|
130
|
+
|
131
|
+
if version > Schema.version
|
132
|
+
warn "setting version to #{version}; latest supported version is #{Schema.version}"
|
133
|
+
end
|
134
|
+
|
135
|
+
attributes[:version] = version
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [Boolean] whether or not the Locale's version is less than CSL-Ruby's default version
|
139
|
+
def legacy?
|
140
|
+
version < Schema.version
|
141
|
+
end
|
142
|
+
|
143
|
+
# call-seq:
|
144
|
+
# locale.set('en') -> sets language to :en, region to :US
|
145
|
+
# locale.set('de-AT') -> sets language to :de, region to :AT
|
146
|
+
# locale.set('-DE') -> sets langauge to :de, region to :DE
|
147
|
+
#
|
148
|
+
# Sets language and region according to the passed-in locale string. If
|
149
|
+
# the region part is not defined by the string, this method will set the
|
150
|
+
# region to the default region for the given language.
|
151
|
+
#
|
152
|
+
# Raises ArgumentError if the argument is no valid locale string. A valid
|
153
|
+
# locale string is based on the syntax of IETF language tags; it consists
|
154
|
+
# of either a language or region tag (or both), separated by a hyphen.
|
155
|
+
def set(locale)
|
156
|
+
language, region = locale.to_s.scan(/([a-z]{2})?(?:-([A-Z]{2}))?/)[0].map do |tag|
|
157
|
+
tag.respond_to?(:to_sym) ? tag.to_sym : nil
|
158
|
+
end
|
159
|
+
|
160
|
+
case
|
161
|
+
when language && region
|
162
|
+
@language, @region = language, region
|
163
|
+
when language
|
164
|
+
@language, @region = language, Locale.regions[language]
|
165
|
+
when region
|
166
|
+
@language, @region = Locale.languages[region], region
|
167
|
+
else
|
168
|
+
raise ArgumentError, "not a valid locale string: #{locale.inspect}"
|
169
|
+
end
|
170
|
+
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
# Sets the locale's language and region to nil.
|
175
|
+
def clear
|
176
|
+
@language, @region = nil
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
def translate(*arguments)
|
181
|
+
raise 'not implemented'
|
182
|
+
end
|
183
|
+
|
184
|
+
alias _ translate
|
185
|
+
alias t translate
|
186
|
+
|
187
|
+
# call-seq:
|
188
|
+
# locale.each_term { |term| block } -> locale
|
189
|
+
# locale.each_term -> enumerator
|
190
|
+
#
|
191
|
+
# Calls block once for each term defined by the locale. If no block is
|
192
|
+
# given, an enumerator is returned instead.
|
193
|
+
def each_term
|
194
|
+
if block_given?
|
195
|
+
terms.each(&Proc.new)
|
196
|
+
self
|
197
|
+
else
|
198
|
+
enum_for :each_term
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# call-seq:
|
203
|
+
# locale.each_date { |date_format| block } -> locale
|
204
|
+
# locale.each_date -> enumerator
|
205
|
+
#
|
206
|
+
# Calls block once for each date format defined by the locale. If no
|
207
|
+
# block is given, an enumerator is returned instead.
|
208
|
+
def each_date
|
209
|
+
if block_given?
|
210
|
+
date.each(&Proc.new)
|
211
|
+
else
|
212
|
+
enum_for :each_date
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# @returns [Boolean] whether or not the Locale is the default locale
|
217
|
+
def default?
|
218
|
+
to_s == Locale.default
|
219
|
+
end
|
220
|
+
|
221
|
+
# @return [Boolean] whehter or not the Locale's region is the default
|
222
|
+
# region for its language
|
223
|
+
def default_region?
|
224
|
+
region && region == Locale.regions[language]
|
225
|
+
end
|
226
|
+
|
227
|
+
# @return [Boolean] whether or not the Locale's language is the default
|
228
|
+
# language for its region
|
229
|
+
def default_language?
|
230
|
+
language && language == Locale.languages[region]
|
231
|
+
end
|
232
|
+
|
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
|
+
# Locales are sorted first by language, then by region; sort order is
|
316
|
+
# alphabetical with the following exceptions: the default locale is
|
317
|
+
# prioritised; in case of a language match the default region of that
|
318
|
+
# language will be prioritised (e.g., de-DE will come before de-AT even
|
319
|
+
# though the alphabetical order would be different).
|
320
|
+
#
|
321
|
+
# @param other [Locale] the locale used for comparison
|
322
|
+
# @return [1,0,-1,nil] the result of the comparison
|
323
|
+
def <=>(other)
|
324
|
+
case
|
325
|
+
when !other.is_a?(Locale)
|
326
|
+
nil
|
327
|
+
when [language, region] == [other.language, other.region]
|
328
|
+
0
|
329
|
+
when default?
|
330
|
+
-1
|
331
|
+
when other.default?
|
332
|
+
1
|
333
|
+
when language == other.language
|
334
|
+
case
|
335
|
+
when default_region?
|
336
|
+
-1
|
337
|
+
when other.default_region?
|
338
|
+
1
|
339
|
+
else
|
340
|
+
region <=> other.region
|
341
|
+
end
|
342
|
+
else
|
343
|
+
language <=> other.language
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# @return [String] the Locale's IETF tag
|
348
|
+
def to_s
|
349
|
+
[language, region].compact.join('-')
|
350
|
+
end
|
351
|
+
|
352
|
+
# @return [String] a string representation of the Locale
|
353
|
+
def inspect
|
354
|
+
"#<#{self.class.name} #{to_s}: dates=[#{dates.length}] terms=[#{terms.length}]>"
|
355
|
+
end
|
356
|
+
|
357
|
+
private
|
358
|
+
|
359
|
+
def attribute_assignments
|
360
|
+
super.push('xml:lang="%s"' % to_s)
|
361
|
+
end
|
362
|
+
|
363
|
+
# @return [Hash] a valid ordinalize query; the name attribute is a format string
|
364
|
+
def ordinalize_query_for(options)
|
365
|
+
q = { :name => 'ordinal-%02d' }
|
366
|
+
|
367
|
+
unless options.nil?
|
368
|
+
if options.key?(:form) && options[:form].to_s =~ /^long(-ordinal)?$/i
|
369
|
+
q[:name] = 'long-ordinal-%02d'
|
370
|
+
end
|
371
|
+
|
372
|
+
gender = (options[:'gender-form'] || options[:gender]).to_s
|
373
|
+
unless gender.empty? || gender =~ /^n/i
|
374
|
+
q[:'gender-form'] = (gender =~ /^m/i) ? 'masculine' : 'feminine'
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
q
|
379
|
+
end
|
380
|
+
|
381
|
+
def legacy_ordinalize(number)
|
382
|
+
case
|
383
|
+
when (11..13).include?(number.abs % 100)
|
384
|
+
[number, terms['ordinal-04']].join
|
385
|
+
when (1..3).include?(number.abs % 10)
|
386
|
+
[number, terms['ordinal-%02d' % (number.abs % 10)]].join
|
387
|
+
else
|
388
|
+
[number, terms['ordinal-04']].join
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CSL
|
2
|
+
class Locale
|
3
|
+
|
4
|
+
# A localized Date comprises a set of formatting rules for dates.
|
5
|
+
class Date < Node
|
6
|
+
|
7
|
+
attr_struct :form, *Schema.attr(:font, :delimiter, :textcase)
|
8
|
+
attr_children :'date-part'
|
9
|
+
|
10
|
+
alias parts date_part
|
11
|
+
alias locale parent
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
super(attributes)
|
15
|
+
children[:'date-part'] = []
|
16
|
+
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def added_to(node)
|
21
|
+
raise ValidationError, "parent must be locale node: was #{node.inspect}" unless node.is_a?(Locale)
|
22
|
+
end
|
23
|
+
|
24
|
+
%w{ text numeric }.each do |type|
|
25
|
+
define_method("#{type}?") { attributes.form == type }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# DatePart represent the localized formatting options for an individual
|
31
|
+
# date part (day, month, or year).
|
32
|
+
class DatePart < Node
|
33
|
+
has_no_children
|
34
|
+
|
35
|
+
attr_struct :name, :form, :'range-delimiter',
|
36
|
+
*Schema.attr(:affixes, :textcase, :font, :periods)
|
37
|
+
|
38
|
+
%w{ day month year }.each do |part|
|
39
|
+
define_method("#{part}?") do
|
40
|
+
attributes.name == part
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|