csl 1.0.0.pre3 → 1.0.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/csl/info.rb +145 -30
- data/lib/csl/node.rb +55 -48
- data/lib/csl/style.rb +91 -35
- data/lib/csl/treelike.rb +83 -84
- data/lib/csl/version.rb +1 -1
- data/spec/csl/info_spec.rb +2 -2
- data/spec/csl/style_spec.rb +56 -13
- data/spec/fixtures/locales/locales-de-DE.xml +304 -0
- data/spec/fixtures/locales/locales-en-GB.xml +304 -0
- data/spec/fixtures/locales/locales-en-US.xml +304 -0
- data/spec/fixtures/styles/apa.csl +443 -0
- data/spec/spec_helper.rb +11 -0
- metadata +16 -8
data/lib/csl/info.rb
CHANGED
@@ -1,73 +1,169 @@
|
|
1
1
|
module CSL
|
2
|
-
|
2
|
+
#
|
3
|
+
# {Info} nodes contain a {Style} (or {Locale}) metadata. Their XML structure
|
4
|
+
# is based on the Atom Syndication Format. For independent styles an {Info}
|
5
|
+
# node typically has the following child elements:
|
6
|
+
#
|
7
|
+
# * {Author} and {Contributor}: used to respectively acknowledge
|
8
|
+
# style authors and contributors, may each be used multiple times.
|
9
|
+
# * {Category}: styles may be assigned one or more categories. One
|
10
|
+
# {Category} node may be used once to describe how in-text citations
|
11
|
+
# are rendered, using its citation-format attribute.
|
12
|
+
# * {Id}: Must appear once. The element should contain a URI to establish
|
13
|
+
# the identity of the style. A stable, unique and dereferenceable URI
|
14
|
+
# is desired for publicly available styles.
|
15
|
+
# * {Link}: Multiple links can be added to an {Info} node: self,
|
16
|
+
# documentation, and template links all have dedicated accessors.
|
17
|
+
# * {Title}: Must appear once. The contents of this node should be the
|
18
|
+
# name of the style as shown to users.
|
19
|
+
# * {TitleShort}: May appear once. The contents of this node should be a
|
20
|
+
# shortened style name (e.g. "APA").
|
21
|
+
# * {Summary}: This node gives a description of the style.
|
22
|
+
# * {Rights}: This node specifies the license under which the style file
|
23
|
+
# is released. The element may carry a license attribute to specify the
|
24
|
+
# URI of the license.
|
25
|
+
# * {Updated}: Must appear once. This node must contain a timestamp that
|
26
|
+
# shows when the style was last updated.
|
27
|
+
#
|
28
|
+
# In dependent styles, the {Info} node must contain a {Link} with rel set
|
29
|
+
# to "independent-parent", with the URI of the independent parent style
|
30
|
+
# set on href. This link is also accessible as a string using the
|
31
|
+
# {#independent_parent} accessors. In addition, dependent styles should
|
32
|
+
# not contain template links.
|
33
|
+
#
|
34
|
+
# In a {Locale} node the {Info} node typically carries only {Translator},
|
35
|
+
# {Rights} and {Updated} nodes.
|
3
36
|
class Info < Node
|
4
37
|
|
5
38
|
attr_children :title, :'title-short', :id, :issn, :eissn, :issnl,
|
6
39
|
:link, :author, :contributor, :category, :published, :summary,
|
7
40
|
:updated, :rights, :'link-dependent-style'
|
8
|
-
|
41
|
+
|
9
42
|
alias_child :contributors, :contributor
|
10
|
-
alias_child :authors, :
|
43
|
+
alias_child :authors, :author
|
11
44
|
alias_child :links, :link
|
12
|
-
|
45
|
+
alias_child :categories, :category
|
46
|
+
|
13
47
|
def initialize(attributes = {})
|
14
48
|
super(attributes, &nil)
|
15
|
-
children[:link] = []
|
49
|
+
children[:link], children[:category] = [], []
|
16
50
|
|
17
51
|
yield self if block_given?
|
18
52
|
end
|
19
|
-
|
53
|
+
|
20
54
|
# @!attribute self_link
|
21
55
|
# @return [String,nil] the style's URI
|
22
56
|
|
23
57
|
# @!attribute template_link
|
24
58
|
# @return [String,nil] URI of the style from which the current style is derived
|
25
|
-
|
59
|
+
|
26
60
|
# @!attribute documentation_link
|
27
61
|
# @return [String,nil] URI of style documentation
|
28
|
-
|
29
|
-
|
30
|
-
|
62
|
+
|
63
|
+
# @!attribute independent_parent_link
|
64
|
+
# @return [String,nil] URI of independent-parent
|
65
|
+
%w{ self template documentation independent-parent }.each do |type|
|
66
|
+
method_id = "#{type.tr('-', '_')}_link"
|
67
|
+
|
31
68
|
define_method method_id do
|
32
|
-
link = links.detect { |l| l.match? :rel => type
|
69
|
+
link = links.detect { |l| l.match? :rel => type }
|
33
70
|
link.nil? ? nil : link[:href]
|
34
71
|
end
|
35
|
-
|
72
|
+
|
36
73
|
alias_method "has_#{method_id}?", method_id
|
37
|
-
|
74
|
+
|
38
75
|
define_method "#{method_id}=" do |value|
|
39
|
-
link = links.detect { |l| l.match? :rel => type
|
40
|
-
|
76
|
+
link = links.detect { |l| l.match? :rel => type }
|
77
|
+
|
41
78
|
if link.nil?
|
42
|
-
set_child_link :href => value.to_s, :rel => type
|
79
|
+
set_child_link :href => value.to_s, :rel => type
|
43
80
|
else
|
44
81
|
link[:href] = value.to_s
|
45
82
|
link
|
46
83
|
end
|
47
84
|
end
|
48
85
|
end
|
49
|
-
|
86
|
+
|
87
|
+
# Ruby 1.8 still has Object#id methods so the attr_children generator
|
88
|
+
# has not created those; since #id is deprecated in 1.8.7 we're
|
89
|
+
# forcing the override anyway. Live dangerously!
|
90
|
+
|
91
|
+
# @return [Id] the id text node
|
50
92
|
def id
|
51
93
|
children[:id]
|
52
94
|
end
|
53
|
-
|
95
|
+
|
54
96
|
alias id= set_child_id
|
55
97
|
|
98
|
+
# @return [Time,nil] when the info node's parent was last updated
|
99
|
+
def updated_at
|
100
|
+
return unless has_updated?
|
101
|
+
updated.to_time
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sets the updated_at timestamp.
|
105
|
+
# @return [self]
|
106
|
+
def update!(timestamp = Time.now)
|
107
|
+
ts = timestamp.respond_to?(:xmlschema) ? timestamp.xmlschema : timestamp.to_s
|
108
|
+
|
109
|
+
if has_updated?
|
110
|
+
updated = Updated.new { |u| u.text = ts }
|
111
|
+
else
|
112
|
+
updated.text = ts
|
113
|
+
end
|
114
|
+
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [Time,nil] when the info node's parent was published
|
119
|
+
def published_at
|
120
|
+
return unless has_published?
|
121
|
+
published.to_time
|
122
|
+
end
|
123
|
+
|
124
|
+
# Sets the updated_at timestamp.
|
125
|
+
# @return [self]
|
126
|
+
def publish!(timestamp = Time.now)
|
127
|
+
ts = timestamp.respond_to?(:xmlschema) ? timestamp.xmlschema : timestamp.to_s
|
128
|
+
|
129
|
+
if has_published?
|
130
|
+
published = Published.new { |u| u.text = ts }
|
131
|
+
else
|
132
|
+
published.text = ts
|
133
|
+
end
|
134
|
+
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [Symbol] the parent style's citation format
|
139
|
+
def citation_format
|
140
|
+
return unless has_categories?
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
def ciation_format=(new_format)
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Info Child Nodes
|
149
|
+
#
|
56
150
|
|
57
151
|
class Contributor < Node
|
58
152
|
attr_children :name, :email, :uri
|
59
153
|
end
|
60
|
-
|
154
|
+
|
61
155
|
class Author < Node
|
62
156
|
attr_children :name, :email, :uri
|
63
157
|
end
|
64
|
-
|
158
|
+
|
65
159
|
class Translator < Node
|
66
160
|
attr_children :name, :email, :uri
|
67
161
|
end
|
68
|
-
|
162
|
+
|
69
163
|
class Link < Node
|
70
164
|
attr_struct :href, :rel
|
165
|
+
|
166
|
+
# TODO xml:lang
|
71
167
|
end
|
72
168
|
|
73
169
|
class DependentStyle < TextNode
|
@@ -77,36 +173,55 @@ module CSL
|
|
77
173
|
|
78
174
|
class Category < Node
|
79
175
|
attr_struct :field, :'citation-format'
|
80
|
-
end
|
176
|
+
end
|
81
177
|
|
82
178
|
class Id < TextNode
|
83
179
|
end
|
84
|
-
|
180
|
+
|
85
181
|
class Name < TextNode
|
86
182
|
end
|
87
|
-
|
183
|
+
|
88
184
|
class Email < TextNode
|
89
185
|
end
|
90
186
|
|
187
|
+
class URI < TextNode
|
188
|
+
end
|
189
|
+
|
91
190
|
class Title < TextNode
|
191
|
+
# TODO xml:lang
|
92
192
|
end
|
93
193
|
|
94
|
-
class
|
194
|
+
class TitleShort < TextNode
|
195
|
+
# TODO xml:lang
|
95
196
|
end
|
96
197
|
|
97
198
|
class Summary < TextNode
|
199
|
+
# TODO xml:lang
|
98
200
|
end
|
99
|
-
|
201
|
+
|
100
202
|
class Rights < TextNode
|
203
|
+
attr_struct :license
|
204
|
+
# TODO xml:lang
|
101
205
|
end
|
102
206
|
|
103
|
-
class
|
207
|
+
class Updated < TextNode
|
208
|
+
|
209
|
+
def to_time
|
210
|
+
return if empty?
|
211
|
+
Time.parse(to_s)
|
212
|
+
end
|
213
|
+
alias to_date to_time
|
104
214
|
end
|
105
215
|
|
106
|
-
class
|
216
|
+
class Published < TextNode
|
217
|
+
def to_time
|
218
|
+
return if empty?
|
219
|
+
Time.parse(to_s)
|
220
|
+
end
|
221
|
+
alias to_date to_time
|
107
222
|
end
|
108
223
|
|
109
224
|
end
|
110
|
-
|
111
|
-
|
225
|
+
|
226
|
+
|
112
227
|
end
|
data/lib/csl/node.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
module CSL
|
2
|
-
|
2
|
+
|
3
3
|
class Node
|
4
|
-
|
4
|
+
|
5
5
|
extend Forwardable
|
6
|
-
|
6
|
+
|
7
7
|
include Enumerable
|
8
8
|
include Comparable
|
9
|
-
|
9
|
+
|
10
10
|
include Treelike
|
11
11
|
include PrettyPrinter
|
12
|
-
|
13
|
-
|
12
|
+
|
13
|
+
|
14
14
|
class << self
|
15
15
|
|
16
16
|
def inherited(subclass)
|
@@ -19,19 +19,19 @@ module CSL
|
|
19
19
|
klass.types << subclass if klass < Node
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def types
|
24
24
|
@types ||= Set.new
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def default_attributes
|
28
28
|
@default_attributes ||= {}
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def constantize(name)
|
32
|
-
pattern =
|
32
|
+
pattern = /:#{name.to_s.tr('-', '')}$/i
|
33
33
|
klass = types.detect { |t| t.matches?(pattern) }
|
34
|
-
|
34
|
+
|
35
35
|
case
|
36
36
|
when !klass.nil?
|
37
37
|
klass
|
@@ -48,16 +48,16 @@ module CSL
|
|
48
48
|
name_pattern === name
|
49
49
|
end
|
50
50
|
alias matches? match?
|
51
|
-
|
51
|
+
|
52
52
|
# Returns a new node with the passed in name and attributes.
|
53
53
|
def create(name, attributes = {}, &block)
|
54
54
|
klass = constantize(name)
|
55
55
|
|
56
|
-
node = (klass || Node).new(attributes, &block)
|
56
|
+
node = (klass || Node).new(attributes, &block)
|
57
57
|
node.nodename = name
|
58
58
|
node
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
def create_attributes(attributes)
|
62
62
|
if const?(:Attributes)
|
63
63
|
const_get(:Attributes).new(default_attributes.merge(attributes))
|
@@ -101,27 +101,27 @@ module CSL
|
|
101
101
|
def values
|
102
102
|
super.compact
|
103
103
|
end
|
104
|
-
|
104
|
+
|
105
105
|
# def to_a
|
106
106
|
# keys.zip(values_at(*keys)).reject { |k,v| v.nil? }
|
107
107
|
# end
|
108
|
-
|
108
|
+
|
109
109
|
# @return [Boolean] true if all the attribute values are nil;
|
110
110
|
# false otherwise.
|
111
111
|
def empty?
|
112
112
|
values.compact.empty?
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
def fetch(key, default = nil)
|
116
116
|
value = keys.include?(key.to_sym) && send(:'[]', key)
|
117
|
-
|
118
|
-
if block_given?
|
117
|
+
|
118
|
+
if block_given?
|
119
119
|
value || yield(key)
|
120
120
|
else
|
121
121
|
value || default
|
122
122
|
end
|
123
123
|
end
|
124
|
-
|
124
|
+
|
125
125
|
# Merges the current with the passed-in attributes.
|
126
126
|
#
|
127
127
|
# @param other [#each_pair] the other attributes
|
@@ -162,14 +162,14 @@ module CSL
|
|
162
162
|
attr_reader :attributes
|
163
163
|
|
164
164
|
def_delegators :attributes, :[], :[]=, :values, :values_at, :length, :size
|
165
|
-
|
165
|
+
|
166
166
|
def initialize(attributes = {})
|
167
167
|
@attributes = self.class.create_attributes(attributes)
|
168
168
|
@children = self.class.create_children
|
169
|
-
|
169
|
+
|
170
170
|
yield self if block_given?
|
171
171
|
end
|
172
|
-
|
172
|
+
|
173
173
|
# Iterates through the Node's attributes
|
174
174
|
def each
|
175
175
|
if block_given?
|
@@ -186,7 +186,7 @@ module CSL
|
|
186
186
|
def attribute?(name)
|
187
187
|
attributes.fetch(name, false)
|
188
188
|
end
|
189
|
-
|
189
|
+
|
190
190
|
# Returns true if the node contains any attributes (ignores nil values);
|
191
191
|
# false otherwise.
|
192
192
|
def has_attributes?
|
@@ -202,7 +202,7 @@ module CSL
|
|
202
202
|
File.open(path, 'w:UTF-8') do |f|
|
203
203
|
f << (options[:compact] ? to_xml : pretty_print)
|
204
204
|
end
|
205
|
-
|
205
|
+
|
206
206
|
self
|
207
207
|
end
|
208
208
|
|
@@ -231,7 +231,7 @@ module CSL
|
|
231
231
|
# @return [Boolean] whether or not the query matches the node
|
232
232
|
def match?(name = nodename, conditions = {})
|
233
233
|
name, conditions = match_conditions_for(name, conditions)
|
234
|
-
|
234
|
+
|
235
235
|
return false unless name === nodename
|
236
236
|
return true if conditions.empty?
|
237
237
|
|
@@ -265,10 +265,10 @@ module CSL
|
|
265
265
|
# @return [Boolean] whether or not the query matches the node exactly
|
266
266
|
def exact_match?(name = nodename, conditions = {})
|
267
267
|
name, conditions = match_conditions_for(name, conditions)
|
268
|
-
|
268
|
+
|
269
269
|
return false unless name === nodename
|
270
270
|
return true if conditions.empty?
|
271
|
-
|
271
|
+
|
272
272
|
conditions.values_at(*attributes.keys).zip(
|
273
273
|
attributes.values_at(*attributes.keys)).all? do |condition, value|
|
274
274
|
condition === value
|
@@ -281,40 +281,40 @@ module CSL
|
|
281
281
|
rescue
|
282
282
|
nil
|
283
283
|
end
|
284
|
-
|
284
|
+
|
285
285
|
# Returns the node' XML tags (including attribute assignments) as an
|
286
286
|
# array of strings.
|
287
287
|
def tags
|
288
288
|
if has_children?
|
289
289
|
tags = []
|
290
290
|
tags << "<#{[nodename, *attribute_assignments].join(' ')}>"
|
291
|
-
|
291
|
+
|
292
292
|
tags << children.map { |node|
|
293
293
|
node.respond_to?(:tags) ? node.tags : [node.to_s]
|
294
294
|
}.flatten(1)
|
295
|
-
|
295
|
+
|
296
296
|
tags << "</#{nodename}>"
|
297
297
|
tags
|
298
298
|
else
|
299
299
|
["<#{[nodename, *attribute_assignments].join(' ')}/>"]
|
300
300
|
end
|
301
301
|
end
|
302
|
-
|
302
|
+
|
303
303
|
def inspect
|
304
304
|
"#<#{[self.class.name, *attribute_assignments].join(' ')} children=[#{children.count}]>"
|
305
305
|
end
|
306
|
-
|
306
|
+
|
307
307
|
alias to_s pretty_print
|
308
308
|
|
309
|
-
|
309
|
+
|
310
310
|
private
|
311
|
-
|
311
|
+
|
312
312
|
def attribute_assignments
|
313
313
|
each_pair.map { |name, value|
|
314
314
|
value.nil? ? nil: [name, value.to_s.inspect].join('=')
|
315
315
|
}.compact
|
316
316
|
end
|
317
|
-
|
317
|
+
|
318
318
|
def match_conditions_for(name, conditions)
|
319
319
|
case name
|
320
320
|
when Hash
|
@@ -327,15 +327,15 @@ module CSL
|
|
327
327
|
end
|
328
328
|
|
329
329
|
end
|
330
|
-
|
331
|
-
|
330
|
+
|
331
|
+
|
332
332
|
class TextNode < Node
|
333
|
-
|
333
|
+
|
334
334
|
has_no_children
|
335
335
|
|
336
336
|
class << self
|
337
337
|
undef_method :attr_children
|
338
|
-
|
338
|
+
|
339
339
|
# @override
|
340
340
|
def create(name, attributes = {}, &block)
|
341
341
|
klass = constantize(name)
|
@@ -347,19 +347,22 @@ module CSL
|
|
347
347
|
end
|
348
348
|
|
349
349
|
attr_accessor :text
|
350
|
-
|
350
|
+
|
351
|
+
def to_s
|
352
|
+
text.to_s.strip
|
353
|
+
end
|
351
354
|
|
352
355
|
# TextNodes quack like a string.
|
353
356
|
# def_delegators :to_s, *String.instance_methods(false).reject do |m|
|
354
357
|
# m.to_s =~ /^\W|!$|(?:^(?:hash|eql?|to_s|length|size|inspect)$)/
|
355
358
|
# end
|
356
|
-
#
|
359
|
+
#
|
357
360
|
# String.instance_methods(false).select { |m| m.to_s =~ /!$/ }.each do |m|
|
358
361
|
# define_method(m) do
|
359
362
|
# content.send(m) if content.respond_to?(m)
|
360
363
|
# end
|
361
364
|
# end
|
362
|
-
|
365
|
+
|
363
366
|
def initialize(argument = '')
|
364
367
|
case
|
365
368
|
when argument.is_a?(Hash)
|
@@ -372,19 +375,23 @@ module CSL
|
|
372
375
|
raise ArgumentError, "failed to create text node from #{argument.inspect}"
|
373
376
|
end
|
374
377
|
end
|
375
|
-
|
378
|
+
|
376
379
|
def textnode?
|
377
380
|
true
|
378
381
|
end
|
379
|
-
|
382
|
+
|
383
|
+
def empty?
|
384
|
+
text.nil? || text.empty?
|
385
|
+
end
|
386
|
+
|
380
387
|
def tags
|
381
388
|
["<#{attribute_assignments.unshift(nodename).join(' ')}>#{text}</#{nodename}>"]
|
382
389
|
end
|
383
|
-
|
390
|
+
|
384
391
|
def inspect
|
385
392
|
"#<#{[self.class.name, text.inspect, *attribute_assignments].join(' ')}>"
|
386
393
|
end
|
387
|
-
|
394
|
+
|
388
395
|
end
|
389
|
-
|
396
|
+
|
390
397
|
end
|