mods_display 1.0.0.alpha4 → 1.0.0.alpha5

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +42 -0
  3. data/.rubocop_todo.yml +72 -401
  4. data/Gemfile +4 -1
  5. data/Rakefile +8 -6
  6. data/app/components/mods_display/field_component.rb +2 -0
  7. data/app/components/mods_display/list_field_component.rb +2 -0
  8. data/app/components/mods_display/record_component.rb +2 -0
  9. data/app/helpers/mods_display/record_helper.rb +4 -7
  10. data/app/models/mods_display/record.rb +2 -1
  11. data/config.ru +2 -2
  12. data/lib/mods_display/country_codes.rb +2 -1
  13. data/lib/mods_display/fields/abstract.rb +2 -0
  14. data/lib/mods_display/fields/access_condition.rb +7 -4
  15. data/lib/mods_display/fields/audience.rb +2 -0
  16. data/lib/mods_display/fields/cartographics.rb +8 -1
  17. data/lib/mods_display/fields/collection.rb +6 -2
  18. data/lib/mods_display/fields/contact.rb +2 -0
  19. data/lib/mods_display/fields/contents.rb +2 -0
  20. data/lib/mods_display/fields/description.rb +3 -2
  21. data/lib/mods_display/fields/extent.rb +3 -0
  22. data/lib/mods_display/fields/field.rb +6 -9
  23. data/lib/mods_display/fields/form.rb +3 -0
  24. data/lib/mods_display/fields/genre.rb +2 -0
  25. data/lib/mods_display/fields/geo.rb +5 -2
  26. data/lib/mods_display/fields/identifier.rb +20 -17
  27. data/lib/mods_display/fields/imprint.rb +168 -217
  28. data/lib/mods_display/fields/language.rb +5 -1
  29. data/lib/mods_display/fields/location.rb +4 -1
  30. data/lib/mods_display/fields/name.rb +35 -19
  31. data/lib/mods_display/fields/nested_related_item.rb +12 -2
  32. data/lib/mods_display/fields/note.rb +9 -7
  33. data/lib/mods_display/fields/related_item.rb +12 -9
  34. data/lib/mods_display/fields/resource_type.rb +2 -0
  35. data/lib/mods_display/fields/sub_title.rb +2 -0
  36. data/lib/mods_display/fields/subject.rb +17 -50
  37. data/lib/mods_display/fields/title.rb +37 -26
  38. data/lib/mods_display/fields/values.rb +2 -0
  39. data/lib/mods_display/html.rb +4 -3
  40. data/lib/mods_display/related_item_concerns.rb +4 -2
  41. data/lib/mods_display/relator_codes.rb +2 -0
  42. data/lib/mods_display/version.rb +3 -1
  43. data/lib/mods_display.rb +5 -3
  44. data/mods_display.gemspec +1 -1
  45. metadata +12 -6
@@ -1,44 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModsDisplay
2
4
  class Imprint < Field
3
5
  include ModsDisplay::CountryCodes
6
+
4
7
  def fields
5
- return_fields = []
6
- @values.each do |value|
7
- if imprint_display_form(value)
8
- return_fields << imprint_display_form(value)
9
- else
10
- edition = edition_element(value)
11
- place = place_element(value)
12
- publisher = publisher_element(value)
13
- parts = parts_element(value)
14
- place_pub = compact_and_join_with_delimiter([place, publisher], ' : ')
15
- edition_place = compact_and_join_with_delimiter([edition, place_pub], ' - ')
16
- joined_place_parts = compact_and_join_with_delimiter([edition_place, parts], ', ')
17
- unless joined_place_parts.empty?
18
- return_fields << ModsDisplay::Values.new(
19
- label: displayLabel(value) || I18n.t('mods_display.imprint'),
20
- values: [joined_place_parts]
21
- )
22
- end
23
- return_fields.concat(dates(value)) if dates(value).length > 0
24
- if other_pub_info(value).length > 0
25
- other_pub_info(value).each do |pub_info|
26
- return_fields << ModsDisplay::Values.new(
27
- label: displayLabel(value) || pub_info_labels[pub_info.name.to_sym],
28
- values: [pub_info.text.strip]
29
- )
30
- end
31
- end
8
+ collapse_fields(origin_info_data.flatten)
9
+ end
10
+
11
+ def origin_info_data
12
+ @values.map do |value|
13
+ return_fields = []
14
+
15
+ edition = edition_element(value)
16
+ place = place_element(value)
17
+ publisher = publisher_element(value)
18
+ parts = parts_element(value)
19
+ place_pub = compact_and_join_with_delimiter([place, publisher], ' : ')
20
+ edition_place = compact_and_join_with_delimiter([edition, place_pub], ' - ')
21
+ joined_place_parts = compact_and_join_with_delimiter([edition_place, parts], ', ')
22
+
23
+ unless joined_place_parts.empty?
24
+ return_fields << ModsDisplay::Values.new(
25
+ label: displayLabel(value) || I18n.t('mods_display.imprint'),
26
+ values: [joined_place_parts]
27
+ )
32
28
  end
29
+ return_fields.concat(date_values(value))
30
+
31
+ other_pub_info(value).each do |pub_info|
32
+ return_fields << ModsDisplay::Values.new(
33
+ label: displayLabel(value) || pub_info_labels[pub_info.name.to_sym],
34
+ values: [pub_info.text.strip]
35
+ )
36
+ end
37
+
38
+ return_fields.compact
33
39
  end
34
- collapse_fields(return_fields)
35
40
  end
36
41
 
37
- def dates(element)
42
+ def date_values(element)
38
43
  date_field_keys.map do |date_field|
39
44
  next unless element.respond_to?(date_field)
45
+
40
46
  elements = element.send(date_field)
41
47
  next if elements.empty?
48
+
42
49
  ModsDisplay::Values.new(
43
50
  label: displayLabel(element) || pub_info_labels[elements.first.name.to_sym],
44
51
  values: parse_dates(elements)
@@ -46,167 +53,147 @@ module ModsDisplay
46
53
  end.compact
47
54
  end
48
55
 
49
- def parse_dates(date_field)
50
- apply_date_qualifier_decoration(
51
- dedup_dates(
52
- join_date_ranges(
53
- process_bc_ad_dates(
54
- process_encoded_dates(ignore_bad_dates(date_field))
55
- )
56
- )
57
- )
58
- )
59
- end
56
+ class DateValue
57
+ attr_reader :value
58
+ delegate :text, :date, :point, :qualifier, :encoding, to: :value
60
59
 
61
- def ignore_bad_dates(date_fields)
62
- date_fields.select do |date_field|
63
- date_field.text.strip != '9999'
64
- end
65
- end
60
+ def initialize(value)
61
+ @value = value
62
+ end
66
63
 
67
- def process_encoded_dates(date_fields)
68
- date_fields.map do |date_field|
69
- if date_is_w3cdtf?(date_field)
70
- process_w3cdtf_date(date_field)
71
- elsif date_is_iso8601?(date_field)
72
- process_iso8601_date(date_field)
73
- else
74
- date_field
64
+ # True if the element text isn't blank or the placeholder "9999".
65
+ def valid?
66
+ text.present? && !['9999', '0000-00-00', 'uuuu'].include?(text.strip)
75
67
  end
76
- end
77
- end
78
68
 
79
- def join_date_ranges(date_fields)
80
- if dates_are_range?(date_fields)
81
- start_date = date_fields.find { |d| d.attributes['point'] && d.attributes['point'].value == 'start' }
82
- end_date = date_fields.find { |d| d.attributes['point'] && d.attributes['point'].value == 'end' }
83
- date_fields.map do |date|
84
- date = date.clone # clone the date object so we don't append the same one
85
- if normalize_date(date.text) == normalize_date(start_date.text)
86
- date.content = [start_date.text, end_date.text].join('-')
87
- date
88
- elsif normalize_date(date.text) != normalize_date(end_date.text)
89
- date
69
+ # Element text reduced to digits and hyphen. Captures date ranges and
70
+ # negative (B.C.) dates. Used for comparison/deduping.
71
+ def base_value
72
+ if text =~ /^\[?1\d{3}-\d{2}\??\]?$/
73
+ return text.sub(/(\d{2})(\d{2})-(\d{2})/, '\1\2-\1\3')
90
74
  end
91
- end.compact
92
- elsif dates_are_open_range?(date_fields)
93
- start_date = date_fields.find { |d| d.attributes['point'] && d.attributes['point'].value == 'start' }
94
- date_fields.map do |date|
95
- date = date.clone # clone the date object so we don't append the same one
96
- date.content = "#{start_date.text}-" if date.text == start_date.text
97
- date
98
- end
99
- else
100
- date_fields
101
- end
102
- end
103
75
 
104
- def apply_date_qualifier_decoration(date_fields)
105
- return_fields = date_fields.map do |date|
106
- date = date.clone
107
- if date_is_approximate?(date)
108
- date.content = "[ca. #{date.text}]"
109
- elsif date_is_questionable?(date)
110
- date.content = "[#{date.text}?]"
111
- elsif date_is_inferred?(date)
112
- date.content = "[#{date.text}]"
76
+ text.gsub(/(?<![\d])(\d{1,3})([xu-]{1,3})/i) { "#{$1}#{'0' * $2.length}"}.scan(/[\d-]/).join
113
77
  end
114
- date
115
- end
116
- return_fields.map(&:text)
117
- end
118
78
 
119
- def date_is_approximate?(date_field)
120
- date_field.attributes['qualifier'] &&
121
- date_field.attributes['qualifier'].respond_to?(:value) &&
122
- date_field.attributes['qualifier'].value == 'approximate'
123
- end
79
+ # Decoded version of the date, if it was encoded. Strips leading zeroes.
80
+ def decoded_value
81
+ return text.strip unless date
124
82
 
125
- def date_is_questionable?(date_field)
126
- date_field.attributes['qualifier'] &&
127
- date_field.attributes['qualifier'].respond_to?(:value) &&
128
- date_field.attributes['qualifier'].value == 'questionable'
129
- end
83
+ unless encoding.present?
84
+ return text.strip unless text =~ /^-?\d+$/ || text =~ /^[\dXxu?-]{4}$/
85
+ end
130
86
 
131
- def date_is_inferred?(date_field)
132
- date_field.attributes['qualifier'] &&
133
- date_field.attributes['qualifier'].respond_to?(:value) &&
134
- date_field.attributes['qualifier'].value == 'inferred'
135
- end
87
+ # Delegate to the appropriate decoding method, if any
88
+ case value.precision
89
+ when :day
90
+ date.strftime('%B %e, %Y')
91
+ when :month
92
+ date.strftime('%B %Y')
93
+ when :year
94
+ year = date.year
95
+ if year < 1
96
+ "#{year.abs + 1} B.C."
97
+ # Any dates before the year 1000 are explicitly marked A.D.
98
+ elsif year > 1 && year < 1000
99
+ "#{year} A.D."
100
+ else
101
+ year.to_s
102
+ end
103
+ when :century
104
+ return "#{(date.to_s[0..1].to_i + 1).ordinalize} century"
105
+ when :decade
106
+ return "#{date.year}s"
107
+ else
108
+ text.strip
109
+ end
110
+ end
136
111
 
137
- def dates_are_open_range?(date_fields)
138
- date_fields.any? do |field|
139
- field.attributes['point'] &&
140
- field.attributes['point'].respond_to?(:value) &&
141
- field.attributes['point'].value == 'start'
142
- end && !date_fields.any? do |field|
143
- field.attributes['point'] &&
144
- field.attributes['point'].respond_to?(:value) &&
145
- field.attributes['point'].value == 'end'
146
- end
147
- end
112
+ # Decoded date with "B.C." or "A.D." and qualifier markers. See (outdated):
113
+ # https://consul.stanford.edu/display/chimera/MODS+display+rules#MODSdisplayrules-3b.%3CoriginInfo%3E
114
+ def qualified_value
115
+ date = decoded_value
116
+
117
+ return "[ca. #{date}]" if qualifier == 'approximate'
118
+ return "[#{date}?]" if qualifier == 'questionable'
119
+ return "[#{date}]" if qualifier == 'inferred'
148
120
 
149
- def dates_are_range?(date_fields)
150
- attributes = date_fields.map do |date|
151
- if date.attributes['point'].respond_to?(:value)
152
- date.attributes['point'].value
121
+ date
153
122
  end
154
123
  end
155
- attributes.include?('start') &&
156
- attributes.include?('end')
157
- end
158
124
 
159
- def date_is_w3cdtf?(date_field)
160
- field_is_encoded?(date_field, 'w3cdtf')
161
- end
125
+ class DateRange
126
+ def initialize(start: nil, stop: nil)
127
+ @start = start
128
+ @stop = stop
129
+ end
162
130
 
163
- def date_is_iso8601?(date_field)
164
- field_is_encoded?(date_field, 'iso8601')
165
- end
131
+ # Base value as hyphen-joined string. Used for comparison/deduping.
132
+ def base_value
133
+ "#{@start&.base_value}-#{@stop&.base_value}"
134
+ end
166
135
 
167
- def process_w3cdtf_date(date_field)
168
- date_field = date_field.clone
169
- date_field.content = begin
170
- if date_field.text.strip =~ /^\d{4}-\d{2}-\d{2}$/
171
- Date.parse(date_field.text).strftime('%B %d, %Y')
172
- elsif date_field.text.strip =~ /^\d{4}-\d{2}$/
173
- Date.parse("#{date_field.text}-01").strftime('%B %Y')
174
- else
175
- date_field.content
136
+ # Base values as array. Used for comparison/deduping of individual dates.
137
+ def base_values
138
+ [@start&.base_value, @stop&.base_value].compact
176
139
  end
177
- rescue
178
- date_field.content
179
- end
180
- date_field
181
- end
182
140
 
183
- def process_iso8601_date(date_field)
184
- date_field = date_field.clone
185
- date_field.content = begin
186
- Date.iso8601(date_field.text).strftime('%B %d, %Y')
187
- rescue
188
- date_field.content
141
+ # The encoding value for the start of the range, or stop if not present.
142
+ def encoding
143
+ @start&.encoding || @stop&.encoding
144
+ end
145
+
146
+ # Decoded dates with "B.C." or "A.D." and qualifier markers applied to
147
+ # the entire range, or individually if dates differ.
148
+ def qualified_value
149
+ if @start&.qualifier == @stop&.qualifier
150
+ qualifier = @start&.qualifier || @stop&.qualifier
151
+ date = "#{@start&.decoded_value}-#{@stop&.decoded_value}"
152
+ return "[ca. #{date}]" if qualifier == 'approximate'
153
+ return "[#{date}?]" if qualifier == 'questionable'
154
+ return "[#{date}]" if qualifier == 'inferred'
155
+
156
+ date
157
+ else
158
+ "#{@start&.qualified_value}-#{@stop&.qualified_value}"
159
+ end
160
+ end
189
161
  end
190
- date_field
191
- end
162
+ def parse_dates(elements)
163
+ # convert to DateValue objects and keep only valid ones
164
+ dates = elements.map(&:as_object).flatten.map { |element| DateValue.new(element) }.select(&:valid?)
165
+
166
+ # join any date ranges into DateRange objects
167
+ point, nonpoint = dates.partition(&:point)
168
+ if point.any?
169
+ range = DateRange.new(start: point.find { |date| date.point == 'start' },
170
+ stop: point.find { |date| date.point == 'end' })
171
+ nonpoint.unshift(range)
172
+ end
173
+ dates = nonpoint
174
+
175
+ # ensure dates are unique with respect to their base values
176
+ dates = dates.group_by(&:base_value).map do |_value, group|
177
+ group.first if group.one?
192
178
 
193
- def dedup_dates(date_fields)
194
- date_text = date_fields.map { |d| normalize_date(d.text) }
195
- if date_text != date_text.uniq
196
- if date_fields.find { |d| d.attributes['qualifier'].respond_to?(:value) }
197
- [date_fields.find { |d| d.attributes['qualifier'].respond_to?(:value) }]
198
- elsif date_fields.find { |d| !d.attributes['encoding'] }
199
- [date_fields.find { |d| !d.attributes['encoding'] }]
179
+ # if one of the duplicates wasn't encoded, use that one. see:
180
+ # https://consul.stanford.edu/display/chimera/MODS+display+rules#MODSdisplayrules-3b.%3CoriginInfo%3E
181
+ if group.reject(&:encoding).any?
182
+ group.reject(&:encoding).first
183
+
184
+ # otherwise just randomly pick the last in the group
200
185
  else
201
- [date_fields.first]
186
+ group.last
202
187
  end
203
- else
204
- date_fields
205
188
  end
206
- end
207
189
 
208
- def normalize_date(date)
209
- date.strip.gsub(/^\[*ca\.\s*|c|\[|\]|\?/, '')
190
+ # if any single dates are already part of a range, discard them
191
+ range_base_values = dates.select { |date| date.is_a?(DateRange) }
192
+ .map(&:base_values).flatten
193
+ dates = dates.reject { |date| range_base_values.include?(date.base_value) }
194
+
195
+ # output formatted dates with qualifiers, A.D./B.C., etc.
196
+ dates.map(&:qualified_value)
210
197
  end
211
198
 
212
199
  def other_pub_info(element)
@@ -218,6 +205,7 @@ module ModsDisplay
218
205
  def place_terms(element)
219
206
  return [] unless element.respond_to?(:place) &&
220
207
  element.place.respond_to?(:placeTerm)
208
+
221
209
  if unencoded_place_terms?(element)
222
210
  element.place.placeTerm.select do |term|
223
211
  !term.attributes['type'].respond_to?(:value) ||
@@ -230,6 +218,7 @@ module ModsDisplay
230
218
  term.attributes['authority'].respond_to?(:value) &&
231
219
  term.attributes['authority'].value == 'marccountry' &&
232
220
  country_codes.include?(term.text.strip)
221
+
233
222
  term = term.clone
234
223
  term.content = country_codes[term.text.strip]
235
224
  term
@@ -244,63 +233,23 @@ module ModsDisplay
244
233
  end
245
234
  end
246
235
 
247
- def imprint_display_form(element)
248
- display_form = element.children.find do |child|
249
- child.name == 'displayForm'
250
- end
251
- ModsDisplay::Values.new(
252
- label: displayLabel(element) || I18n.t('mods_display.imprint'),
253
- values: [display_form.text]
254
- ) if display_form
255
- end
256
-
257
236
  private
258
237
 
259
238
  def compact_and_join_with_delimiter(values, delimiter)
260
239
  compact_values = values.compact.reject { |v| v.strip.empty? }
261
240
  return compact_values.join(delimiter) if compact_values.length == 1 ||
262
241
  !ends_in_terminating_punctuation?(delimiter)
242
+
263
243
  compact_values.each_with_index.map do |value, i|
264
- if (compact_values.length - 1) == i || # last item?
265
- ends_in_terminating_punctuation?(value)
266
- value << ' '
267
- else
268
- value << delimiter
269
- end
244
+ value << if (compact_values.length - 1) == i || # last item?
245
+ ends_in_terminating_punctuation?(value)
246
+ ' '
247
+ else
248
+ delimiter
249
+ end
270
250
  end.join.strip
271
251
  end
272
252
 
273
- def process_bc_ad_dates(date_fields)
274
- date_fields.map do |date_field|
275
- case
276
- when date_is_bc_edtf?(date_field)
277
- year = date_field.text.strip.gsub(/^-0*/, '').to_i + 1
278
- date_field.content = "#{year} B.C."
279
- when date_is_ad?(date_field)
280
- date_field.content = "#{date_field.text.strip.gsub(/^0*/, '')} A.D."
281
- end
282
- date_field
283
- end
284
- end
285
-
286
- def date_is_bc_edtf?(date_field)
287
- date_field.text.strip.start_with?('-') && date_is_edtf?(date_field)
288
- end
289
-
290
- def date_is_ad?(date_field)
291
- date_field.text.strip.gsub(/^0*/, '').length < 4
292
- end
293
-
294
- def date_is_edtf?(date_field)
295
- field_is_encoded?(date_field, 'edtf')
296
- end
297
-
298
- def field_is_encoded?(field, encoding)
299
- field.attributes['encoding'] &&
300
- field.attributes['encoding'].respond_to?(:value) &&
301
- field.attributes['encoding'].value.downcase == encoding
302
- end
303
-
304
253
  def ends_in_terminating_punctuation?(value)
305
254
  value.strip.end_with?('.', ',', ':', ';')
306
255
  end
@@ -313,6 +262,7 @@ module ModsDisplay
313
262
 
314
263
  def place_element(value)
315
264
  return if value.place.text.strip.empty?
265
+
316
266
  places = place_terms(value).reject do |p|
317
267
  p.text.strip.empty?
318
268
  end.map(&:text)
@@ -321,6 +271,7 @@ module ModsDisplay
321
271
 
322
272
  def publisher_element(value)
323
273
  return if value.publisher.text.strip.empty?
274
+
324
275
  publishers = value.publisher.reject do |p|
325
276
  p.text.strip.empty?
326
277
  end.map(&:text)
@@ -328,8 +279,9 @@ module ModsDisplay
328
279
  end
329
280
 
330
281
  def parts_element(value)
331
- date_elements = %w(dateIssued dateOther).map do |date_field_name|
282
+ date_elements = %w[dateIssued dateOther].map do |date_field_name|
332
283
  next unless value.respond_to?(date_field_name.to_sym)
284
+
333
285
  parse_dates(value.send(date_field_name.to_sym))
334
286
  end.flatten.compact.reject do |date|
335
287
  date.strip.empty?
@@ -338,11 +290,11 @@ module ModsDisplay
338
290
  end
339
291
 
340
292
  def pub_info_parts
341
- [:issuance, :frequency]
293
+ %i[issuance frequency]
342
294
  end
343
295
 
344
296
  def date_field_keys
345
- [:dateCreated, :dateCaptured, :dateValid, :dateModified, :copyrightDate]
297
+ %i[dateCreated dateCaptured dateValid dateModified copyrightDate]
346
298
  end
347
299
 
348
300
  def pub_info_labels
@@ -352,8 +304,7 @@ module ModsDisplay
352
304
  dateModified: I18n.t('mods_display.date_modified'),
353
305
  copyrightDate: I18n.t('mods_display.copyright_date'),
354
306
  issuance: I18n.t('mods_display.issuance'),
355
- frequency: I18n.t('mods_display.frequency')
356
- }
307
+ frequency: I18n.t('mods_display.frequency') }
357
308
  end
358
309
  end
359
310
  end
@@ -1,13 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModsDisplay
2
4
  class Language < Field
3
5
  def fields
4
6
  return_fields = @values.map do |value|
5
7
  next unless value.respond_to?(:languageTerm)
8
+
6
9
  value.languageTerm.map do |term|
7
10
  next unless term.attributes['type'].respond_to?(:value) && term.attributes['type'].value == 'code'
11
+
8
12
  ModsDisplay::Values.new(
9
13
  label: displayLabel(value) || displayLabel(term) || I18n.t('mods_display.language'),
10
- values: [displayForm(value) || language_codes[term.text]].flatten
14
+ values: [language_codes[term.text]]
11
15
  )
12
16
  end.flatten.compact
13
17
  end.flatten.compact
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModsDisplay
2
4
  class Location < Field
3
5
  def fields
@@ -5,6 +7,7 @@ module ModsDisplay
5
7
  @values.each do |location|
6
8
  location.children.each do |child|
7
9
  next unless location_field_keys.include?(child.name.to_sym)
10
+
8
11
  if child.name.to_sym == :url
9
12
  loc_label = displayLabel(location) || I18n.t('mods_display.location')
10
13
  value = "<a href='#{child.text}'>#{(displayLabel(child) || child.text).gsub(/:$/, '')}</a>"
@@ -24,7 +27,7 @@ module ModsDisplay
24
27
  private
25
28
 
26
29
  def location_field_keys
27
- [:physicalLocation, :url, :shelfLocator, :holdingSimple, :holdingExternal]
30
+ %i[physicalLocation url shelfLocator holdingSimple holdingExternal]
28
31
  end
29
32
 
30
33
  def location_label(element)
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModsDisplay
2
4
  class Name < Field
3
5
  include ModsDisplay::RelatorCodes
4
6
  def fields
5
7
  return_fields = @values.map do |value|
6
- person = if value.displayForm.length > 0
8
+ person = if value.displayForm.length.positive?
7
9
  ModsDisplay::Name::Person.new(name: value.displayForm.text)
8
10
  elsif !name_parts(value).empty?
9
11
  ModsDisplay::Name::Person.new(name: name_parts(value))
@@ -40,11 +42,25 @@ module ModsDisplay
40
42
  def role_labels(element)
41
43
  default_label = I18n.t('mods_display.associated_with')
42
44
  return [default_label] unless element.role.present? && element.role.roleTerm.present?
43
- element.role.roleTerm.collect do |role|
44
- relator_codes[role.text.downcase] || role.text.capitalize || default_label
45
+
46
+ element.role.collect do |role|
47
+ codes, text = role.roleTerm.partition { |term| term['type'] == 'code' }
48
+
49
+ # prefer mappable role term codes
50
+ label = codes.map { |term| relator_codes[term.text.downcase] }.first
51
+
52
+ # but fall back to given text
53
+ label ||= text.map { |term| format_role(term) }.first
54
+
55
+ # or just the default
56
+ label || default_label
45
57
  end.uniq
46
58
  end
47
59
 
60
+ def format_role(element)
61
+ element.text.capitalize.sub(/[.,:;]+$/, '')
62
+ end
63
+
48
64
  def role?(element)
49
65
  element.respond_to?(:role) && !element.role.empty?
50
66
  end
@@ -55,10 +71,9 @@ module ModsDisplay
55
71
  end
56
72
 
57
73
  def name_parts(element)
58
- output = ''
59
- output << [unqualified_name_parts(element),
60
- qualified_name_parts(element, 'family'),
61
- qualified_name_parts(element, 'given')].flatten.compact.join(', ')
74
+ output = [unqualified_name_parts(element),
75
+ qualified_name_parts(element, 'family'),
76
+ qualified_name_parts(element, 'given')].flatten.compact.join(', ')
62
77
  terms = qualified_name_parts(element, 'termsOfAddress')
63
78
  unless terms.empty?
64
79
  term_delimiter = ', '
@@ -66,9 +81,7 @@ module ModsDisplay
66
81
  output = [output, terms.join(', ')].flatten.compact.join(term_delimiter)
67
82
  end
68
83
  dates = qualified_name_parts(element, 'date')
69
- unless dates.empty?
70
- output = [output, qualified_name_parts(element, 'date')].flatten.compact.join(', ')
71
- end
84
+ output = [output, qualified_name_parts(element, 'date')].flatten.compact.join(', ') unless dates.empty?
72
85
  output
73
86
  end
74
87
 
@@ -90,7 +103,7 @@ module ModsDisplay
90
103
  def name_part_begins_with_roman_numeral?(part)
91
104
  first_part = part.split(/\s|,/).first.strip
92
105
  first_part.chars.all? do |char|
93
- %w(I X C L V).include? char
106
+ %w[I X C L V].include? char
94
107
  end
95
108
  end
96
109
 
@@ -98,14 +111,16 @@ module ModsDisplay
98
111
  roles = element.role.map do |role|
99
112
  role.roleTerm.find do |term|
100
113
  term.attributes['type'].respond_to?(:value) &&
101
- term.attributes['type'].value == 'text'
114
+ term.attributes['type'].value == 'text'
102
115
  end
103
116
  end.compact
104
- roles = element.role.map do |role|
105
- role.roleTerm.find do |term|
106
- !term.attributes['type'].respond_to?(:value)
107
- end
108
- end.compact if roles.empty?
117
+ if roles.empty?
118
+ roles = element.role.map do |role|
119
+ role.roleTerm.find do |term|
120
+ !term.attributes['type'].respond_to?(:value)
121
+ end
122
+ end.compact
123
+ end
109
124
  roles.map { |t| t.text.strip }
110
125
  end
111
126
 
@@ -130,7 +145,7 @@ module ModsDisplay
130
145
  # Normalize label headings by filtering out some punctuation and ending in :
131
146
  def normalize_labels(label_order, results)
132
147
  label_order.uniq.map do |k|
133
- label = k.tr('.', '').tr(':', '').strip + ':'
148
+ label = "#{k.tr('.', '').tr(':', '').strip}:"
134
149
  if label != k
135
150
  results[label] = results[k]
136
151
  results.delete(k)
@@ -152,8 +167,9 @@ module ModsDisplay
152
167
 
153
168
  class Person
154
169
  attr_accessor :name
170
+
155
171
  def initialize(data)
156
- @name = data[:name]
172
+ @name = data[:name]
157
173
  end
158
174
 
159
175
  def to_s