bibtex-ruby 1.3.12 → 2.0.0pre1

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.

Potentially problematic release.


This version of bibtex-ruby might be problematic. Click here for more details.

@@ -28,13 +28,22 @@ module BibTeX
28
28
  attr_reader :bibliography
29
29
 
30
30
  # Returns an array of BibTeX elements.
31
- def self.parse(string, options = {})
32
- elements = BibTeX::Parser.new(options).parse(string).data
33
- elements.each do |e|
34
- e.parse_names unless !e.respond_to?(:parse_names) || options[:parse_names] == false
35
- e.parse_month unless !e.respond_to?(:parse_month) || options[:parse_months] == false
36
- end
37
- elements
31
+ def self.parse(input, options = {})
32
+ case input
33
+ when Element
34
+ [input]
35
+ when Hash
36
+ [Entry.new(input)]
37
+ when Array
38
+ input.inject([]) { |s,a| s.concat(parse(a, options)) }
39
+ when ::String
40
+ Parser.new(options).parse(input).data.each do |e|
41
+ e.parse_names unless !e.respond_to?(:parse_names) || options[:parse_names] == false
42
+ e.parse_month unless !e.respond_to?(:parse_month) || options[:parse_months] == false
43
+ end
44
+ else
45
+ raise BibTeXError, "failed to parse Element from #{input.inspect}"
46
+ end
38
47
  end
39
48
 
40
49
  # Returns a string containing the object's content.
@@ -47,24 +56,26 @@ module BibTeX
47
56
  def join; self; end
48
57
 
49
58
  # Returns the element's id.
50
- def id; @id ||= object_id.to_s.intern; end
59
+ def id; @id ||= object_id.to_s; end
51
60
 
52
61
  # Returns the BibTeX type (if applicable) or the normalized class name.
53
62
  def type
54
63
  self.class.name.split(/::/).last.gsub(/([[:lower:]])([[:upper:]])/) { "#{$1}_#{$2}" }.downcase.intern
55
64
  end
56
65
 
66
+ # Returns a list of names for that Element. All Elements except Entries return an empty list.
67
+ def names
68
+ []
69
+ end
70
+
57
71
  def has_type?(type)
58
72
  self.type == type.intern || defined?(type) == 'constant' && is_a?(type)
59
73
  end
60
74
 
61
75
  [:entry, :book, :article, :collection, :string, :preamble, :comment]. each do |type|
62
- method_id = "#{type}?"
63
- unless method_defined?(method_id)
64
- define_method(method_id) { has_type?(type) }
65
- alias_method("is_#{method_id}", method_id)
66
- end
67
- end
76
+ method_id = "#{type}?"
77
+ define_method(method_id) { has_type?(type) } unless method_defined?(method_id)
78
+ end
68
79
 
69
80
  # Returns true if the element matches the given query.
70
81
  def matches?(query)
@@ -72,7 +83,7 @@ module BibTeX
72
83
 
73
84
  case query
74
85
  when Symbol
75
- query == id
86
+ query.to_s == id.to_s
76
87
  when Element
77
88
  query == self
78
89
  when Regexp
@@ -84,7 +95,7 @@ module BibTeX
84
95
  when /^\/(.+)\/$/
85
96
  to_s.match(Regexp.new($1))
86
97
  else
87
- id == query.to_sym
98
+ id.to_s == query
88
99
  end
89
100
  end
90
101
 
@@ -95,7 +106,7 @@ module BibTeX
95
106
  def meets?(*conditions)
96
107
  conditions.flatten.all? do |condition|
97
108
  property, value = condition.split(/\s*=\s*/)
98
- property.nil? || send(property).to_s == value
109
+ property.nil? || (respond_to?(property) && send(property).to_s == value)
99
110
  end
100
111
  end
101
112
 
@@ -105,7 +116,7 @@ module BibTeX
105
116
 
106
117
  def to_hash(options = {})
107
118
  { type => content }
108
- end
119
+ end
109
120
 
110
121
  def to_yaml(options = {})
111
122
  require 'yaml'
@@ -113,8 +124,7 @@ module BibTeX
113
124
  end
114
125
 
115
126
  def to_json(options = {})
116
- require 'json'
117
- to_hash.to_json
127
+ MultiJson.encode(to_hash(options))
118
128
  end
119
129
 
120
130
  def to_xml(options = {})
@@ -126,6 +136,7 @@ module BibTeX
126
136
 
127
137
  # Called when the element was added to a bibliography.
128
138
  def added_to_bibliography(bibliography)
139
+ # raise BibTeXError, "failed to add element to Bibliography: already registered with another Bibliography" unless @bibliography.nil?
129
140
  @bibliography = bibliography
130
141
  self
131
142
  end
@@ -141,6 +152,10 @@ module BibTeX
141
152
  [type, to_s] <=> [other.type, other.to_s]
142
153
  end
143
154
 
155
+ # Returns the Element as a nicely formatted string.
156
+ def inspect
157
+ "#<#{self.class} #{content.gsub(/\n/, ' ')}>"
158
+ end
144
159
  end
145
160
 
146
161
 
@@ -169,11 +184,11 @@ module BibTeX
169
184
  def key=(key)
170
185
  raise(ArgumentError, "keys must be convertible to Symbol; was: #{type.class.name}.") unless type.respond_to?(:to_sym)
171
186
 
172
- unless @bibliography.nil?
173
- @bibliography.strings.delete(@key)
174
- @bibliography.strings[key.to_sym] = self
175
- end
176
-
187
+ unless bibliography.nil?
188
+ bibliography.strings.delete(@key)
189
+ bibliography.strings[key.to_sym] = self
190
+ end
191
+
177
192
  @key = key.to_sym
178
193
  end
179
194
 
@@ -214,11 +229,15 @@ module BibTeX
214
229
  def to_xml(options = {})
215
230
  require 'rexml/document'
216
231
 
217
- xml = REXML::Element.new(:string)
218
- key = REXML::Element.new(:key)
219
- val = REXML::Element.new(:value)
220
- key.text = @key.to_s
221
- val.text = @value.to_s(:quotes => '"')
232
+ xml = REXML::Element.new(:string)
233
+
234
+ k, v = REXML::Element.new(:key), REXML::Element.new(:value)
235
+ k.text = key.to_s
236
+ v.text = value.to_s(:quotes => '"')
237
+
238
+ xml.add_elements(k)
239
+ xml.add_elements(v)
240
+
222
241
  xml
223
242
  end
224
243
  end
@@ -26,7 +26,7 @@ module BibTeX
26
26
  extend Forwardable
27
27
  include Enumerable
28
28
 
29
- # Hash containing the required fields of the standard entry types
29
+ # Defines the required fields of the standard entry types
30
30
  REQUIRED_FIELDS = Hash.new([]).merge({
31
31
  :article => [:author,:title,:journal,:year],
32
32
  :book => [[:author,:editor],:title,:publisher,:year],
@@ -44,6 +44,13 @@ module BibTeX
44
44
  :unpublished => [:author,:title,:note]
45
45
  }).freeze
46
46
 
47
+ # Defines the default fallbacks for values defined in cross-references
48
+ FIELD_ALIASES = {
49
+ :booktitle => :title,
50
+ # :editor => :author
51
+ }.freeze
52
+
53
+
47
54
  NAME_FIELDS = [:author,:editor,:translator].freeze
48
55
  DATE_FIELDS = [:year,:month].freeze
49
56
 
@@ -102,13 +109,14 @@ module BibTeX
102
109
  }.map(&:intern)]).freeze
103
110
 
104
111
 
105
- attr_reader :fields, :type
112
+ attr_reader :fields, :type
113
+
106
114
  def_delegators :@fields, :empty?, :each, :each_pair
107
115
 
108
116
  # Creates a new instance. If a hash is given, the entry is populated accordingly.
109
117
  def initialize(attributes = {})
110
118
  @fields = {}
111
-
119
+
112
120
  self.type = attributes.delete(:type) if attributes.has_key?(:type)
113
121
  self.key = attributes.delete(:key) if attributes.has_key?(:key)
114
122
 
@@ -125,17 +133,29 @@ module BibTeX
125
133
 
126
134
  add(other.fields)
127
135
  end
128
-
129
- # Sets the key of the entry
136
+
137
+ # Returns the Entry's field name aliases.
138
+ def aliases
139
+ @aliases ||= FIELD_ALIASES.dup
140
+ end
141
+
142
+ # Sets the Entry's key. If the Entry is currently registered with a
143
+ # Bibliography, re-registers the Entry with the new key; note that this
144
+ # may change the key value if another Entry is already regsitered with
145
+ # the same key.
146
+ #
147
+ # Returns the new key.
130
148
  def key=(key)
131
- raise(ArgumentError, "keys must be convertible to Symbol; was: #{type.class.name}.") unless type.respond_to?(:to_sym)
132
-
133
- unless @bibliography.nil?
134
- @bibliography.entries.delete(@key)
135
- @bibliography.entries[key] = self
149
+ key = key.to_s
150
+
151
+ if registered?
152
+ bibliography.entries.delete(@key)
153
+ key = register(key)
136
154
  end
137
155
 
138
- @key = key.to_sym
156
+ @key = key
157
+ rescue => e
158
+ raise BibTeXError, "failed to set key to #{key.inspect}: #{e.message}"
139
159
  end
140
160
 
141
161
  def key
@@ -148,7 +168,7 @@ module BibTeX
148
168
  # TODO we should be more lenient: allow strings as key or don't check at all
149
169
  # Sets the type of the entry.
150
170
  def type=(type)
151
- raise(ArgumentError, "types must be convertible to Symbol; was: #{type.class.name}.") unless type.respond_to?(:to_sym)
171
+ # raise(ArgumentError, "types must be convertible to Symbol; was: #{type.class.name}.") unless type.respond_to?(:to_sym)
152
172
  @type = type.to_sym
153
173
  end
154
174
 
@@ -156,25 +176,92 @@ module BibTeX
156
176
  type.to_s.match(/^entry$/i) || @type == type.to_sym || super
157
177
  end
158
178
 
159
- def has_field?(field)
160
- @fields.has_key?(field)
179
+ def has_field?(name)
180
+ name.respond_to?(:to_sym) ? fields.has_key?(name.to_sym) : false
161
181
  end
182
+
183
+ def inherits?(name)
184
+ !has_field(name) && has_parent? && parent.provides?(name)
185
+ end
186
+
187
+ # Returns true if the Entry has a field (or alias) for the passed-in name.
188
+ def provides?(name)
189
+ return nil unless name.respond_to?(:to_sym)
190
+ has_field?(name) || has_field?(aliases[name.to_sym])
191
+ end
162
192
 
193
+ # Returns the field value referenced by the passed-in name.
194
+ # For example, this will return the 'title' value for 'booktitle' if a
195
+ # corresponding alias is defined.
196
+ def provide(name)
197
+ return nil unless name.respond_to?(:to_sym)
198
+ name = name.to_sym
199
+ fields[name] || fields[aliases[name]]
200
+ end
201
+
202
+ # If the Entry has a cross-reference, copies all referenced all inherited
203
+ # values from the parent.
204
+ #
205
+ # Returns the Entry.
206
+ def save_inherited_fields
207
+ inherited_fields.each do |name|
208
+ fields[name] = parent.provide(name)
209
+ end
210
+
211
+ self
212
+ end
213
+
214
+ # Returns a sorted list of the Entry's field names. If a +filter+ is passed
215
+ # as argument, returns all field names that are also defined by the filter.
216
+ # If the +filter+ is empty, returns all field names.
217
+ #
218
+ # If the second optional argument is true (default) and the Entry contains
219
+ # a cross-reference, the list will include all inherited fields.
220
+ def field_names(filter = [], include_inherited = true)
221
+ names = fields.keys
222
+
223
+ if include_inherited && has_parent?
224
+ names.concat(inherited_fields)
225
+ end
226
+
227
+ unless filter.empty?
228
+ names = names & filter.map(&:to_sym)
229
+ end
230
+
231
+ names.sort!
232
+ names
233
+ end
234
+
235
+ # Returns a sorted list of all field names referenced by this Entry's cross-reference.
236
+ def inherited_fields
237
+ return [] unless has_parent?
238
+
239
+ names = parent.fields.keys - fields.keys
240
+ names.concat(parent.aliases.reject { |k,v| !parent.has_field?(v) }.keys)
241
+ names.sort!
242
+
243
+ names
244
+ end
245
+
246
+
163
247
  def method_missing(name, *args, &block)
164
248
  case
165
- when @fields.has_key?(name)
166
- @fields[name]
249
+ when fields.has_key?(name)
250
+ fields[name]
167
251
  when name.to_s =~ /^(.+)=$/
168
252
  send(:add, $1.to_sym, args[0])
169
253
  when name =~ /^(?:convert|from)_([a-z]+)(!)?$/
170
254
  $2 ? convert!($1, &block) : convert($1, &block)
255
+ when has_parent? && parent.provides?(name)
256
+ parent.provide(name)
171
257
  else
172
258
  super
173
259
  end
174
260
  end
175
261
 
176
262
  def respond_to?(method)
177
- @fields.has_key?(method.to_sym) || method.to_s.match(/=$/) || method =~ /^(?:convert|from)_([a-z]+)(!)?$/ || super
263
+ provides?(method.to_sym) || method.to_s.match(/=$/) ||
264
+ method =~ /^(?:convert|from)_([a-z]+)(!)?$/ || (has_parent? && parent.respond_to(method)) || super
178
265
  end
179
266
 
180
267
  # Returns a copy of the Entry with all the field names renamed.
@@ -186,20 +273,28 @@ module BibTeX
186
273
  # exists.
187
274
  def rename!(*arguments)
188
275
  Hash[*arguments.flatten].each_pair do |from,to|
189
- if @fields.has_key?(from) && !@fields.has_key?(to)
190
- @fields[to] = @fields[from]
191
- @fields.delete(from)
276
+ if fields.has_key?(from) && !fields.has_key?(to)
277
+ fields[to] = fields[from]
278
+ fields.delete(from)
192
279
  end
193
280
  end
194
281
  self
195
282
  end
196
283
 
197
- alias :rename_fields :rename
198
- alias :rename_fields! :rename!
284
+ alias rename_fields rename
285
+ alias rename_fields! rename!
199
286
 
200
- # Returns the value of the field with the given name.
287
+ # Returns the value of the field with the given name. If the value is not
288
+ # defined and the entry has cross-reference, returns the cross-referenced
289
+ # value instead.
201
290
  def [](name)
202
- @fields[name.to_sym]
291
+ fields[name.to_sym] || parent && parent.provide(name)
292
+ end
293
+
294
+ alias get []
295
+
296
+ def fetch(name, default = nil)
297
+ get(name) || block_given? ? yield(name) : default
203
298
  end
204
299
 
205
300
  # Adds a new field (name-value pair) to the entry.
@@ -216,11 +311,11 @@ module BibTeX
216
311
  # add(:author, "Edgar A. Poe", :title, "The Raven")
217
312
  # add([:author, "Edgar A. Poe", :title, "The Raven"])
218
313
  # add(:author => "Edgar A. Poe", :title => "The Raven")
219
- #
220
314
  def add(*arguments)
221
315
  Hash[*arguments.flatten].each_pair do |name, value|
222
- @fields[name.to_sym] = Value.new(value)
316
+ fields[name.to_sym] = Value.new(value)
223
317
  end
318
+
224
319
  self
225
320
  end
226
321
 
@@ -229,34 +324,64 @@ module BibTeX
229
324
  # Removes the field with a given name from the entry.
230
325
  # Returns the value of the deleted field; nil if the field was not set.
231
326
  def delete(name)
232
- @fields.delete(name.to_sym)
327
+ fields.delete(name.to_sym)
233
328
  end
234
329
 
235
330
  # Returns false if the entry is one of the standard entry types and does not have
236
331
  # definitions of all the required fields for that type.
237
332
  def valid?
238
333
  REQUIRED_FIELDS[@type].all? do |f|
239
- f.is_a?(Array) ? !(f & @fields.keys).empty? : !@fields[f].nil?
334
+ f.is_a?(Array) ? !(f & fields.keys).empty? : !fields[f].nil?
240
335
  end
241
336
  end
242
337
 
338
+ def generate_hash(filter = [])
339
+ Digest::MD5.hexdigest(field_names(filter).map { |k| [k, fields[k]] }.flatten.join)
340
+ end
341
+
243
342
  # Called when the element was added to a bibliography.
244
343
  def added_to_bibliography(bibliography)
245
344
  super
246
- bibliography.entries[key] = self
247
- parse_names if bibliography.options[:parse_names]
248
- parse_months if bibliography.options[:parse_months]
249
- convert(bibliography.options[:filter]) if bibliography.options[:filter]
345
+
346
+ @key = register(key)
347
+
348
+ [:parse_names, :parse_months].each do |parser|
349
+ send(parser) if bibliography.options[parser]
350
+ end
351
+
352
+ if bibliography.options.has_key?(:filter)
353
+ convert!(bibliography.options[:filter])
354
+ end
355
+
250
356
  self
251
357
  end
252
-
358
+
253
359
  # Called when the element was removed from a bibliography.
254
360
  def removed_from_bibliography(bibliography)
255
361
  super
256
- bibliography.entries[key] = nil
362
+ bibliography.entries.delete(key)
257
363
  self
258
364
  end
259
365
 
366
+ # Returns true if the Entry is currently registered with the associated Bibliography.
367
+ def registered?
368
+ !!(bibliography && bibliography.entries[key].equal?(self))
369
+ end
370
+
371
+ # Registers this Entry in the associated Bibliographies entries hash.
372
+ # This method may change the Entry's key, if another entry is already
373
+ # registered with the current key.
374
+ #
375
+ # Returns the key or nil if the Entry is not associated with a Bibliography.
376
+ def register(key)
377
+ return nil if bibliography.nil?
378
+
379
+ k = key.dup
380
+ k.succ! while bibliography.has_key?(k)
381
+ bibliography.entries[k] = self
382
+ k
383
+ end
384
+
260
385
  def replace(*arguments)
261
386
  arguments = bibliography.q('@string') if arguments.empty?
262
387
  @fields.values.each { |v| v.replace(*arguments) }
@@ -269,11 +394,11 @@ module BibTeX
269
394
  end
270
395
 
271
396
  def month=(month)
272
- @fields[:month] = MONTHS_FILTER[month]
397
+ fields[:month] = MONTHS_FILTER[month]
273
398
  end
274
399
 
275
400
  def parse_month
276
- @fields[:month] = MONTHS_FILTER[@fields[:month]] if @fields.has_key?(:month)
401
+ fields[:month] = MONTHS_FILTER[fields[:month]] if fields.has_key?(:month)
277
402
  self
278
403
  end
279
404
 
@@ -284,18 +409,65 @@ module BibTeX
284
409
  def parse_names
285
410
  strings = bibliography ? bibliography.strings.values : []
286
411
  NAME_FIELDS.each do |key|
287
- if name = @fields[key]
288
- name.replace(strings).join
289
- name = name.to_name
290
- @fields[key] = name
412
+ if name = fields[key]
413
+ name = name.dup.replace(strings).join.to_name
414
+ fields[key] = name unless name.nil?
291
415
  end
292
416
  end
293
417
  self
294
418
  end
295
419
 
420
+ # Returns a list of all names (authors, editors, translators).
421
+ def names
422
+ NAME_FIELDS.map { |k| has_field?(k) ? @fields[k].tokens : nil }.flatten.compact
423
+ end
424
+
425
+
426
+ # Returns true if the Entry has a valid cross-reference in the Bibliography.
427
+ def has_parent?
428
+ !parent.nil?
429
+ end
430
+
431
+ alias has_cross_reference? has_parent?
432
+
433
+ # Returns true if the Entry cross-references an Entry which is not
434
+ # registered in the current Bibliography.
435
+ def parent_missing?
436
+ has_field?(:crossref) && !has_parent?
437
+ end
438
+
439
+ alias cross_reference_missing? parent_missing?
440
+
441
+ # Returns the cross-referenced Entry from the Bibliography or nil if this
442
+ # Entry does define a cross-reference.
443
+ def parent
444
+ bibliography && bibliography[fields[:crossref]]
445
+ end
446
+
447
+ alias cross_reference parent
448
+
449
+
450
+ # Returns true if the entry is cross-referenced by another entry in the
451
+ # Bibliography.
452
+ def has_children?
453
+ !children.empty?
454
+ end
455
+
456
+ alias cross_referenced? has_children?
457
+
458
+ # Returns a list of all entries in the Bibliography containing a
459
+ # cross-reference to this entry or [] if there are no references to this
460
+ # entry.
461
+ def children
462
+ (bibliography && bibliography.q("@entry[crossref=#{key}]")) || []
463
+ end
464
+
465
+ alias cross_referenced_by children
466
+
467
+
296
468
  # Returns a string of all the entry's fields.
297
469
  def content(options = {})
298
- @fields.map { |k,v| "#{k} = #{ @fields[k].to_s(options) }" }.join(",\n")
470
+ fields.map { |k,v| "#{k} = #{ fields[k].to_s(options) }" }.join(",\n")
299
471
  end
300
472
 
301
473
  # Returns a string representation of the entry.
@@ -332,27 +504,37 @@ module BibTeX
332
504
  def to_xml(options = {})
333
505
  require 'rexml/document'
334
506
 
335
- xml = REXML::Element.new(type)
336
- xml.attributes['key'] = key
337
- @fields.each do |k,v|
338
- e = REXML::Element.new(k.to_s)
339
- e.text = v.to_s(options)
340
- xml.add_element(e)
507
+ xml = REXML::Element.new('bibtex:entry')
508
+ xml.attributes['id'] = key
509
+
510
+ entry = REXML::Element.new("bibtex:#{type}")
511
+
512
+ fields.each do |key, value|
513
+ field = REXML::Element.new("bibtex:#{key}")
514
+
515
+ if options[:extended] && value.name?
516
+ value.each { |n| entry.add_element(n.to_xml) }
517
+ else
518
+ field.text = value.to_s(options)
519
+ end
520
+
521
+ entry.add_element(field)
341
522
  end
342
- xml
523
+
524
+ xml.add_element(entry)
525
+ xml
343
526
  end
344
527
 
345
528
  # Returns a duplicate entry with all values converted using the filter.
346
529
  # If an optional block is given, only those values will be converted where
347
530
  # the block returns true (the block will be called with each key-value pair).
348
531
  #
349
- # @see convert!
350
- #
532
+ # @see #convert!
351
533
  def convert (filter)
352
534
  block_given? ? dup.convert!(filter, &Proc.new) : dup.convert!(filter)
353
535
  end
354
536
 
355
- # In-place variant of @see convert
537
+ # In-place variant of @see #convert
356
538
  def convert! (filter)
357
539
  @fields.each_pair { |k,v| !block_given? || yield(k,v) ? v.convert!(filter) : v }
358
540
  self
@@ -361,29 +543,18 @@ module BibTeX
361
543
  def <=>(other)
362
544
  type != other.type ? type <=> other.type : key != other.key ? key <=> other.key : to_s <=> other.to_s
363
545
  end
546
+
547
+ private
364
548
 
365
- protected
366
-
549
+ # Returns a default key for this entry.
367
550
  def default_key
368
- a = case fields[:author]
369
- when Names
370
- author[0].last
371
- when Value
372
- author.to_s[/\w+/]
373
- else
374
- nil
375
- end
376
-
377
- case
378
- when a && has_field?(:year) && has_field?(:title)
379
- [a,year,title.to_s[/\w{4,}/]].join.downcase.to_sym
380
- when a && has_field?(:year)
381
- [a,year].join.downcase.to_sym
382
- when has_field?(:year) && has_field?(:title)
383
- [year,title.to_s[/\w{4,}/]].join.downcase.to_sym
384
- else
385
- object_id.to_s
386
- end
551
+ k = names[0]
552
+ k = k.respond_to?(:family) ? k.family : k.to_s
553
+ k = k[/[A-Za-z]+/] || 'unknown'
554
+ k << (has_field?(:year) ? year : '-')
555
+ k << 'a'
556
+ k.downcase!
557
+ k
387
558
  end
388
559
 
389
560
  end