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
@@ -0,0 +1,185 @@
|
|
1
|
+
module CSL
|
2
|
+
class Locale
|
3
|
+
|
4
|
+
class Terms < Node
|
5
|
+
attr_children :term
|
6
|
+
|
7
|
+
alias terms term
|
8
|
+
def_delegators :terms, :size, :length
|
9
|
+
|
10
|
+
undef_method :[]=
|
11
|
+
|
12
|
+
def initialize(attributes = {})
|
13
|
+
super(attributes)
|
14
|
+
@registry, children[:term] = Hash.new { |h,k| h[k] = [] }, []
|
15
|
+
|
16
|
+
yield self if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
alias each each_child
|
20
|
+
|
21
|
+
def lookup(query)
|
22
|
+
terms = if Regexp === query
|
23
|
+
registry.keys.select { |t| t =~ query }.flatten(1)
|
24
|
+
else
|
25
|
+
registry[(Hash === query) ? query[:name] : query.to_s]
|
26
|
+
end
|
27
|
+
|
28
|
+
terms.detect { |t| t.matches?(query) }
|
29
|
+
end
|
30
|
+
|
31
|
+
alias [] lookup
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @!attribute [r] registry
|
36
|
+
# @return [Hash] a private registry to map term names to the respective
|
37
|
+
# term objects for quick term look-up
|
38
|
+
attr_reader :registry
|
39
|
+
|
40
|
+
def added_child(term)
|
41
|
+
raise ValidationError, "failed to register term #{term.inspect}: name attribute missing" unless
|
42
|
+
term.attribute?(:name)
|
43
|
+
|
44
|
+
registry[term[:name]].push(term)
|
45
|
+
term
|
46
|
+
end
|
47
|
+
|
48
|
+
def deleted_child(term)
|
49
|
+
registry[term[:name]].delete(term)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Term < Node
|
54
|
+
attr_struct :name, :form, :gender, :'gender-form'
|
55
|
+
attr_children :single, :multiple
|
56
|
+
|
57
|
+
attr_accessor :text
|
58
|
+
|
59
|
+
def_delegators :attributes, :hash, :eql?, :name, :form, :gender
|
60
|
+
|
61
|
+
def gendered?
|
62
|
+
!attributes.gender.blank?
|
63
|
+
end
|
64
|
+
|
65
|
+
def neutral?
|
66
|
+
!gendered?
|
67
|
+
end
|
68
|
+
|
69
|
+
def textnode?
|
70
|
+
!text.blank?
|
71
|
+
end
|
72
|
+
|
73
|
+
def singularize
|
74
|
+
return text if textnode?
|
75
|
+
children.single.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
alias singular singularize
|
79
|
+
|
80
|
+
def pluralize
|
81
|
+
return text if textnode?
|
82
|
+
children.multiple.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
alias plural pluralize
|
86
|
+
|
87
|
+
# Tests whether or not the Term matches the passed-in query. Tests
|
88
|
+
# vary slightly depending on the the query's type: if a String or
|
89
|
+
# Regexp is passed-in, the return value will be true if the Term's
|
90
|
+
# name matches the query (disregarding all other attributes); if the
|
91
|
+
# query is a Hash, however, the return value will only be true if
|
92
|
+
# all passed-in attributes
|
93
|
+
#
|
94
|
+
# @param query [Hash,Regexp,#to_s] the query
|
95
|
+
# @raise [ArgumentError] if the term cannot be matched using query
|
96
|
+
#
|
97
|
+
# @return [Boolean] whether or not the query matches the term
|
98
|
+
def match?(query)
|
99
|
+
case
|
100
|
+
when query.is_a?(Hash)
|
101
|
+
query.symbolize_keys.values_at(*attributes.keys) == attributes.values_at(*attributes.keys)
|
102
|
+
when query.is_a?(Regexp)
|
103
|
+
query =~ name
|
104
|
+
when query.respond_to?(:to_s)
|
105
|
+
query.to_s == name
|
106
|
+
else
|
107
|
+
raise ArgumentError, "cannot match term to query: #{query.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
alias matches? match?
|
111
|
+
|
112
|
+
|
113
|
+
# @!method masculine?
|
114
|
+
# @return [Boolean] whether or not the term is masculine
|
115
|
+
|
116
|
+
# @!method masculine!
|
117
|
+
# @return [self,nil] the term with the gender attribute set to
|
118
|
+
# 'masculine', or nil if the term was already masculine
|
119
|
+
|
120
|
+
# @!method feminine?
|
121
|
+
# @return [Boolean] whether or not the term is feminie
|
122
|
+
|
123
|
+
# @!method feminine!
|
124
|
+
# @return [self,nil] the term with the gender attribute set to
|
125
|
+
# 'feminine', or nil if the term was already feminine
|
126
|
+
%w{ masculine feminine }.each do |name|
|
127
|
+
define_method("#{name}?") do
|
128
|
+
attributes.gender.to_s == name
|
129
|
+
end
|
130
|
+
|
131
|
+
define_method("#{name}!") do
|
132
|
+
return nil if attributes.gender.to_s == name
|
133
|
+
attributes.gender = name
|
134
|
+
self
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def tags
|
139
|
+
if textnode?
|
140
|
+
["<#{[nodename, *attribute_assignments].join(' ')}>", text, "</#{nodename}>"]
|
141
|
+
else
|
142
|
+
super
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# @param options [Hash,nil] an optional configuration hash
|
147
|
+
#
|
148
|
+
# @option options [:singular,:plural] :number (:singular) whether to
|
149
|
+
# return the term's singular or plural variant.
|
150
|
+
#
|
151
|
+
# @return [String] the term as a string
|
152
|
+
def to_s(options = nil)
|
153
|
+
if textnode?
|
154
|
+
text
|
155
|
+
else
|
156
|
+
if pluralize?(options)
|
157
|
+
pluralize
|
158
|
+
else
|
159
|
+
singularize
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class Single < TextNode; end
|
165
|
+
class Multiple < TextNode; end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def pluralize?(options)
|
170
|
+
return false if options.nil?
|
171
|
+
|
172
|
+
key = options[:number] || options['number']
|
173
|
+
|
174
|
+
if key.is_a?(Fixnum) || key.to_s =~ /^[+-]?\d+$/
|
175
|
+
key.to_i > 1
|
176
|
+
else
|
177
|
+
!key.blank? && key.to_s =~ /^plural/i
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
TextNode.types << Term
|
184
|
+
end
|
185
|
+
end
|
data/lib/csl/node.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
module CSL
|
2
|
+
|
3
|
+
class Node
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
include Treelike
|
11
|
+
include PrettyPrinter
|
12
|
+
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def inherited(subclass)
|
17
|
+
types << subclass
|
18
|
+
subclass.nesting.each do |klass|
|
19
|
+
klass.types << subclass if klass < Node
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def types
|
24
|
+
@types ||= Set.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_attributes
|
28
|
+
@default_attributes ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def constantize(name)
|
32
|
+
types.detect do |t|
|
33
|
+
t.name.split(/::/)[-1].gsub(/([[:lower:]])([[:upper:]])/, '\1-\2').downcase == name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a new node with the passed in name and attributes.
|
38
|
+
def create(name, attributes = {}, &block)
|
39
|
+
klass = constantize(name)
|
40
|
+
|
41
|
+
unless klass.nil?
|
42
|
+
klass.new(attributes, &block)
|
43
|
+
else
|
44
|
+
node = new(attributes, &block)
|
45
|
+
node.nodename = name
|
46
|
+
node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_attributes(attributes)
|
51
|
+
if const?(:Attributes)
|
52
|
+
const_get(:Attributes).new(default_attributes.merge(attributes))
|
53
|
+
else
|
54
|
+
default_attributes.merge(attributes)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def attr_defaults(attributes)
|
61
|
+
@default_attributes = attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates a new Struct for the passed-in attributes. Node instances
|
65
|
+
# will create an instance of this struct to manage their respective
|
66
|
+
# attributes.
|
67
|
+
#
|
68
|
+
# The new Struct will be available as Attributes in the current node's
|
69
|
+
# class scope.
|
70
|
+
def attr_struct(*attributes)
|
71
|
+
const_set(:Attributes, Struct.new(*attributes) {
|
72
|
+
|
73
|
+
# 1.8 Compatibility
|
74
|
+
@keys = attributes.map(&:to_sym).freeze
|
75
|
+
|
76
|
+
class << self
|
77
|
+
attr_reader :keys
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(attrs = {})
|
81
|
+
super(*attrs.symbolize_keys.values_at(*keys))
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [<Symbol>] a list of symbols representing the names/keys
|
85
|
+
# of the attribute variables.
|
86
|
+
def keys
|
87
|
+
self.class.keys
|
88
|
+
end
|
89
|
+
|
90
|
+
def values
|
91
|
+
super.compact
|
92
|
+
end
|
93
|
+
|
94
|
+
# def to_a
|
95
|
+
# keys.zip(values_at(*keys)).reject { |k,v| v.nil? }
|
96
|
+
# end
|
97
|
+
|
98
|
+
# @return [Boolean] true if all the attribute values are nil;
|
99
|
+
# false otherwise.
|
100
|
+
def empty?
|
101
|
+
values.compact.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
def fetch(key, default = nil)
|
105
|
+
value = keys.include?(key.to_sym) && send(key)
|
106
|
+
|
107
|
+
if block_given?
|
108
|
+
value || yield(key)
|
109
|
+
else
|
110
|
+
value || default
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Merges the current with the passed-in attributes.
|
115
|
+
#
|
116
|
+
# @param other [#each_pair] the other attributes
|
117
|
+
# @return [self]
|
118
|
+
def merge(other)
|
119
|
+
raise ArgumentError, "failed to merge #{other.class} into Attributes" unless
|
120
|
+
other.respond_to?(:each_pair)
|
121
|
+
|
122
|
+
other.each_pair do |part, value|
|
123
|
+
writer = "#{part}="
|
124
|
+
send(writer, value) if !value.nil? && respond_to?(writer)
|
125
|
+
end
|
126
|
+
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
# @overload values_at(selector, ... )
|
131
|
+
# Returns an array containing the attributes in self according
|
132
|
+
# to the given selector(s). The selectors may be either integer
|
133
|
+
# indices, ranges (functionality inherited from Struct) or
|
134
|
+
# symbols idenifying valid keys (similar to Hash#values_at).
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# attributes.values_at(:family, :nick) #=> ['Matsumoto', 'Matz']
|
138
|
+
#
|
139
|
+
# @see Struct#values_at
|
140
|
+
# @return [Array] the list of values
|
141
|
+
def values_at(*arguments)
|
142
|
+
super(*arguments.flatten.map { |k| k.is_a?(Symbol) ? keys.index(k) : k })
|
143
|
+
end
|
144
|
+
|
145
|
+
})
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
attr_reader :attributes
|
152
|
+
|
153
|
+
def_delegators :attributes, :[], :[]=, :values, :values_at, :length, :size
|
154
|
+
|
155
|
+
def initialize(attributes = {})
|
156
|
+
@attributes = self.class.create_attributes(attributes)
|
157
|
+
@children = self.class.create_children
|
158
|
+
|
159
|
+
yield self if block_given?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Iterates through the Node's attributes
|
163
|
+
def each
|
164
|
+
if block_given?
|
165
|
+
attributes.each_pair(&Proc.new)
|
166
|
+
self
|
167
|
+
else
|
168
|
+
to_enum
|
169
|
+
end
|
170
|
+
end
|
171
|
+
alias each_pair each
|
172
|
+
|
173
|
+
# Returns true if the node contains an attribute with the passed-in name;
|
174
|
+
# false otherwise.
|
175
|
+
def attribute?(name)
|
176
|
+
attributes.fetch(name, false)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns true if the node contains any attributes (ignores nil values);
|
180
|
+
# false otherwise.
|
181
|
+
def has_attributes?
|
182
|
+
!attributes.empty?
|
183
|
+
end
|
184
|
+
|
185
|
+
def textnode?
|
186
|
+
false
|
187
|
+
end
|
188
|
+
alias has_text? textnode?
|
189
|
+
|
190
|
+
def <=>(other)
|
191
|
+
[nodename, attributes, children] <=> [other.nodename, other.attributes, other.children]
|
192
|
+
rescue
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns the node' XML tags (including attribute assignments) as an
|
197
|
+
# array of strings.
|
198
|
+
def tags
|
199
|
+
if has_children?
|
200
|
+
tags = []
|
201
|
+
tags << "<#{[nodename, *attribute_assignments].join(' ')}>"
|
202
|
+
|
203
|
+
tags << children.map do |node|
|
204
|
+
node.respond_to?(:tags) ? node.tags : [node.to_s]
|
205
|
+
end
|
206
|
+
|
207
|
+
tags << "</#{nodename}>"
|
208
|
+
tags
|
209
|
+
else
|
210
|
+
["<#{[nodename, *attribute_assignments].join(' ')}/>"]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def inspect
|
215
|
+
"#<#{[self.class.name, *attribute_assignments].join(' ')} children=[#{children.length}]>"
|
216
|
+
end
|
217
|
+
|
218
|
+
alias to_s pretty_print
|
219
|
+
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def attribute_assignments
|
224
|
+
each_pair.map { |name, value|
|
225
|
+
value.nil? ? nil: [name, value.to_s.inspect].join('=')
|
226
|
+
}.compact
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
class TextNode < Node
|
233
|
+
|
234
|
+
has_no_children
|
235
|
+
|
236
|
+
class << self
|
237
|
+
undef_method :attr_children
|
238
|
+
end
|
239
|
+
|
240
|
+
attr_accessor :text
|
241
|
+
alias to_s text
|
242
|
+
|
243
|
+
# TextNodes quack like a string.
|
244
|
+
# def_delegators :to_s, *String.instance_methods(false).reject do |m|
|
245
|
+
# m.to_s =~ /^\W|!$|(?:^(?:hash|eql?|to_s|length|size|inspect)$)/
|
246
|
+
# end
|
247
|
+
#
|
248
|
+
# String.instance_methods(false).select { |m| m.to_s =~ /!$/ }.each do |m|
|
249
|
+
# define_method(m) do
|
250
|
+
# content.send(m) if content.respond_to?(m)
|
251
|
+
# end
|
252
|
+
# end
|
253
|
+
|
254
|
+
def initialize(argument = '')
|
255
|
+
case
|
256
|
+
when argument.is_a?(Hash)
|
257
|
+
super
|
258
|
+
when argument.respond_to?(:to_s)
|
259
|
+
super({})
|
260
|
+
@text = argument.to_s
|
261
|
+
yield self if block_given?
|
262
|
+
else
|
263
|
+
raise ArgumentError, "failed to create text node from #{argument.inspect}"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def textnode?
|
268
|
+
true
|
269
|
+
end
|
270
|
+
|
271
|
+
def tags
|
272
|
+
tags = []
|
273
|
+
tags << "<#{attribute_assignments.unshift(nodename).join(' ')}>"
|
274
|
+
tags << text
|
275
|
+
tags << "</#{nodename}>"
|
276
|
+
tags
|
277
|
+
end
|
278
|
+
|
279
|
+
def inspect
|
280
|
+
"#<#{[self.class.name, text.inspect, *attribute_assignments].join(' ')}>"
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|