csl 1.0.0.pre3 → 1.0.0.pre4
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/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
|