csl 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.document +5 -0
  2. data/.gitignore +8 -0
  3. data/.gitmodules +6 -0
  4. data/.rspec +3 -0
  5. data/.simplecov +2 -0
  6. data/.travis.yml +13 -0
  7. data/.yardopts +2 -0
  8. data/AGPL +662 -0
  9. data/BSDL +29 -0
  10. data/Gemfile +24 -0
  11. data/Guardfile +14 -0
  12. data/README.md +39 -0
  13. data/Rakefile +45 -0
  14. data/csl.gemspec +36 -0
  15. data/cucumber.yml +1 -0
  16. data/features/locales/loading.feature +57 -0
  17. data/features/locales/ordinalize.feature +861 -0
  18. data/features/parser/info.feature +27 -0
  19. data/features/parser/localized_dates.feature +35 -0
  20. data/features/parser/terms.feature +28 -0
  21. data/features/step_definitions/locale_steps.rb +34 -0
  22. data/features/step_definitions/parser_steps.rb +28 -0
  23. data/features/step_definitions/style_steps.rb +16 -0
  24. data/features/style/loading.feature +53 -0
  25. data/features/support/env.rb +8 -0
  26. data/lib/csl.rb +54 -0
  27. data/lib/csl/compatibility.rb +19 -0
  28. data/lib/csl/errors.rb +15 -0
  29. data/lib/csl/extensions.rb +63 -0
  30. data/lib/csl/info.rb +40 -0
  31. data/lib/csl/loader.rb +78 -0
  32. data/lib/csl/locale.rb +393 -0
  33. data/lib/csl/locale/date.rb +48 -0
  34. data/lib/csl/locale/style_options.rb +10 -0
  35. data/lib/csl/locale/term.rb +185 -0
  36. data/lib/csl/node.rb +285 -0
  37. data/lib/csl/parser.rb +92 -0
  38. data/lib/csl/pretty_printer.rb +33 -0
  39. data/lib/csl/schema.rb +109 -0
  40. data/lib/csl/style.rb +53 -0
  41. data/lib/csl/style/bibliography.rb +15 -0
  42. data/lib/csl/style/citation.rb +17 -0
  43. data/lib/csl/style/conditional.rb +11 -0
  44. data/lib/csl/style/date.rb +16 -0
  45. data/lib/csl/style/group.rb +9 -0
  46. data/lib/csl/style/label.rb +14 -0
  47. data/lib/csl/style/layout.rb +10 -0
  48. data/lib/csl/style/macro.rb +9 -0
  49. data/lib/csl/style/names.rb +54 -0
  50. data/lib/csl/style/number.rb +33 -0
  51. data/lib/csl/style/sort.rb +21 -0
  52. data/lib/csl/style/text.rb +10 -0
  53. data/lib/csl/treelike.rb +442 -0
  54. data/lib/csl/version.rb +3 -0
  55. data/spec/csl/info_spec.rb +116 -0
  56. data/spec/csl/locale/date_spec.rb +63 -0
  57. data/spec/csl/locale/style_options_spec.rb +19 -0
  58. data/spec/csl/locale/term_spec.rb +96 -0
  59. data/spec/csl/locale_spec.rb +128 -0
  60. data/spec/csl/node_spec.rb +100 -0
  61. data/spec/csl/parser_spec.rb +92 -0
  62. data/spec/csl/schema_spec.rb +70 -0
  63. data/spec/csl/style/bibliography_spec.rb +7 -0
  64. data/spec/csl/style/citation_spec.rb +7 -0
  65. data/spec/csl/style/conditional_spec.rb +7 -0
  66. data/spec/csl/style/date_spec.rb +11 -0
  67. data/spec/csl/style/group_spec.rb +7 -0
  68. data/spec/csl/style/label_spec.rb +7 -0
  69. data/spec/csl/style/layout_spec.rb +7 -0
  70. data/spec/csl/style/macro_spec.rb +7 -0
  71. data/spec/csl/style/names_spec.rb +23 -0
  72. data/spec/csl/style/number_spec.rb +84 -0
  73. data/spec/csl/style/text_spec.rb +7 -0
  74. data/spec/csl/style_spec.rb +19 -0
  75. data/spec/csl/treelike_spec.rb +151 -0
  76. data/spec/spec_helper.rb +30 -0
  77. 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
@@ -0,0 +1,10 @@
1
+ module CSL
2
+ class Locale
3
+
4
+ class StyleOptions < Node
5
+ has_no_children
6
+ attr_defaults :'punctuation-in-quote' => false
7
+ end
8
+
9
+ end
10
+ end