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