mods_display 1.0.0.alpha4 → 1.0.0.alpha5

Sign up to get free protection for your applications and to get access to all the features.
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