csl 1.0.0.pre1
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/.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
|