cocina_display 1.1.3 → 1.2.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.standard.yml +1 -1
  4. data/README.md +21 -2
  5. data/config/i18n-tasks.yml +0 -0
  6. data/config/licenses.yml +59 -0
  7. data/config/locales/en.yml +109 -0
  8. data/lib/cocina_display/cocina_record.rb +27 -63
  9. data/lib/cocina_display/concerns/accesses.rb +78 -0
  10. data/lib/cocina_display/concerns/contributors.rb +32 -11
  11. data/lib/cocina_display/concerns/events.rb +19 -6
  12. data/lib/cocina_display/concerns/forms.rb +98 -11
  13. data/lib/cocina_display/concerns/geospatial.rb +9 -5
  14. data/lib/cocina_display/concerns/identifiers.rb +15 -4
  15. data/lib/cocina_display/concerns/languages.rb +6 -2
  16. data/lib/cocina_display/concerns/notes.rb +36 -0
  17. data/lib/cocina_display/concerns/related_resources.rb +20 -0
  18. data/lib/cocina_display/concerns/subjects.rb +25 -8
  19. data/lib/cocina_display/concerns/titles.rb +67 -25
  20. data/lib/cocina_display/concerns/{access.rb → url_helpers.rb} +3 -3
  21. data/lib/cocina_display/concerns.rb +6 -0
  22. data/lib/cocina_display/contributors/contributor.rb +47 -26
  23. data/lib/cocina_display/contributors/name.rb +18 -14
  24. data/lib/cocina_display/contributors/role.rb +20 -13
  25. data/lib/cocina_display/dates/date.rb +55 -14
  26. data/lib/cocina_display/dates/date_range.rb +0 -2
  27. data/lib/cocina_display/description/access.rb +41 -0
  28. data/lib/cocina_display/description/access_contact.rb +11 -0
  29. data/lib/cocina_display/description/url.rb +17 -0
  30. data/lib/cocina_display/display_data.rb +104 -0
  31. data/lib/cocina_display/events/event.rb +8 -4
  32. data/lib/cocina_display/events/imprint.rb +0 -10
  33. data/lib/cocina_display/events/location.rb +0 -2
  34. data/lib/cocina_display/events/note.rb +33 -0
  35. data/lib/cocina_display/forms/form.rb +71 -0
  36. data/lib/cocina_display/forms/genre.rb +12 -0
  37. data/lib/cocina_display/forms/resource_type.rb +38 -0
  38. data/lib/cocina_display/geospatial.rb +1 -1
  39. data/lib/cocina_display/identifier.rb +101 -0
  40. data/lib/cocina_display/json_backed_record.rb +27 -0
  41. data/lib/cocina_display/language.rb +9 -11
  42. data/lib/cocina_display/license.rb +32 -0
  43. data/lib/cocina_display/note.rb +103 -0
  44. data/lib/cocina_display/related_resource.rb +74 -0
  45. data/lib/cocina_display/subjects/subject.rb +32 -9
  46. data/lib/cocina_display/subjects/subject_value.rb +34 -16
  47. data/lib/cocina_display/title.rb +194 -0
  48. data/lib/cocina_display/utils.rb +4 -4
  49. data/lib/cocina_display/version.rb +1 -1
  50. data/lib/cocina_display.rb +30 -2
  51. metadata +45 -11
  52. data/lib/cocina_display/title_builder.rb +0 -397
  53. /data/lib/cocina_display/vocabularies/{marc_country_codes.rb → marc_country.rb} +0 -0
  54. /data/lib/cocina_display/vocabularies/{marc_relator_codes.rb → marc_relator.rb} +0 -0
@@ -0,0 +1,194 @@
1
+ module CocinaDisplay
2
+ # A group of related {TitleValue}s associated with an item.
3
+ class Title
4
+ # The underlying Cocina hash.
5
+ attr_reader :cocina
6
+
7
+ # Type of the title, e.g. "uniform", "alternative", etc.
8
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#title-types
9
+ # @return [String, nil]
10
+ attr_accessor :type
11
+
12
+ # Status of the title, e.g. "primary".
13
+ # @return [String, nil]
14
+ attr_accessor :status
15
+
16
+ # Create a new Title object.
17
+ # @param cocina [Hash]
18
+ # @param part_label [String, nil] part label for digital serials
19
+ # @param part_numbers [Array<String>] part numbers for related resources
20
+ def initialize(cocina, part_label: nil, part_numbers: nil)
21
+ @cocina = cocina
22
+ @part_label = part_label
23
+ @part_numbers = part_numbers
24
+ @type = cocina["type"].presence
25
+ @status = cocina["status"].presence
26
+ end
27
+
28
+ # Label used when displaying the title.
29
+ # @return [String]
30
+ def label
31
+ cocina["displayLabel"].presence || type_label
32
+ end
33
+
34
+ # Does this title have a type?
35
+ # @return [Boolean]
36
+ def type?
37
+ type.present?
38
+ end
39
+
40
+ # Is this marked as a primary title?
41
+ # @return [Boolean]
42
+ def primary?
43
+ status == "primary"
44
+ end
45
+
46
+ # The string representation of the title, for display.
47
+ # @see #display_title
48
+ # @return [String, nil]
49
+ def to_s
50
+ display_title
51
+ end
52
+
53
+ # The short form of the title, without subtitle, part name, etc.
54
+ # @note This corresponds to the "short title" in MODS XML, or MARC 245$a only.
55
+ # @return [String, nil]
56
+ # @example "M. de Courville"
57
+ def short_title
58
+ short_title_str.presence || cocina["value"]
59
+ end
60
+
61
+ # The long form of the title, including subtitle, part name, etc.
62
+ # @note This corresponds to the entire MARC 245 field.
63
+ # @return [String, nil]
64
+ # @example "M. de Courville [estampe]"
65
+ def full_title
66
+ full_title_str.presence || cocina["value"]
67
+ end
68
+
69
+ # The long form of the title, with added punctuation between parts if not present.
70
+ # @note This corresponds to the entire MARC 245 field.
71
+ # @return [String, nil]
72
+ # @example "M. de Courville : [estampe]"
73
+ def display_title
74
+ display_title_str.presence || cocina["value"]
75
+ end
76
+
77
+ # A string value for sorting by title.
78
+ # Ignores punctuation, leading/trailing spaces, and non-sorting characters.
79
+ # If no title is present, returns a high Unicode value so it sorts last.
80
+ # @return [String]
81
+ def sort_title
82
+ return "\u{10FFFF}" unless full_title
83
+
84
+ full_title[nonsorting_char_count..]
85
+ .unicode_normalize(:nfd) # Prevent accents being stripped
86
+ .gsub(/[[:punct:]]*/, "")
87
+ .gsub(/\W{2,}/, " ") # Collapse whitespace after removing punctuation
88
+ .strip
89
+ end
90
+
91
+ private
92
+
93
+ # Generate the short title by joining main title and nonsorting characters with spaces.
94
+ # @return [String, nil]
95
+ def short_title_str
96
+ Utils.compact_and_join([nonsorting_chars_str, main_title_str])
97
+ end
98
+
99
+ # Generate the full title by joining all title components with spaces.
100
+ # @return [String, nil]
101
+ def full_title_str
102
+ Utils.compact_and_join([nonsorting_chars_str, main_title_str, subtitle_str, parts_str])
103
+ end
104
+
105
+ # Generate the display title by joining all components with punctuation:
106
+ # - Join main title and subtitle with " : "
107
+ # - Join part name/number/label with ", "
108
+ # - Join part string with preceding title with ". "
109
+ # - Prepend nonsorting characters with specified padding
110
+ # - Prepend associated names with ". "
111
+ # @return [String, nil]
112
+ def display_title_str
113
+ title_str = Utils.compact_and_join([main_title_str, subtitle_str], delimiter: " : ")
114
+ title_str = Utils.compact_and_join([title_str, parts_str(delimiter: ", ")], delimiter: ". ")
115
+ title_str = Utils.compact_and_join([nonsorting_chars_str, title_str]) if nonsorting_chars_str.present?
116
+ title_str = Utils.compact_and_join([names_str, title_str], delimiter: ". ") if names_str.present?
117
+ title_str.presence
118
+ end
119
+
120
+ # All nonsorting characters joined together with padding applied.
121
+ # @return [String, nil]
122
+ def nonsorting_chars_str
123
+ Utils.compact_and_join(Array(title_components["nonsorting characters"])).ljust(nonsorting_char_count, " ")
124
+ end
125
+
126
+ # The main title component(s), joined together.
127
+ # @return [String, nil]
128
+ def main_title_str
129
+ Utils.compact_and_join(Array(title_components["main title"]))
130
+ end
131
+
132
+ # The subtitle components, joined together.
133
+ # @return [String, nil]
134
+ def subtitle_str
135
+ Utils.compact_and_join(Array(title_components["subtitle"]))
136
+ end
137
+
138
+ # The part name, number, and label components, joined together.
139
+ # Default delimiter is a space, but can be overridden.
140
+ # @return [String, nil]
141
+ def parts_str(delimiter: " ")
142
+ Utils.compact_and_join(
143
+ Array(title_components["part number"] || @part_numbers) +
144
+ Array(title_components["part name"]) +
145
+ [@part_label],
146
+ delimiter: delimiter
147
+ )
148
+ end
149
+
150
+ # The associated names, joined together with periods.
151
+ # @note Only present for uniform titles.
152
+ # @return [String, nil]
153
+ def names_str
154
+ Utils.compact_and_join(names, delimiter: ". ")
155
+ end
156
+
157
+ # Destructured title components, organized by type.
158
+ # Unstructured titles and components with no type are grouped under "main title".
159
+ # @return [Hash<String, Array<String>>]
160
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#title-part-types-for-structured-value
161
+ def title_components
162
+ Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
163
+ type = case node["type"]
164
+ when "uniform", "alternative", "abbreviated", "translated", "transliterated", "parallel", "supplied", nil
165
+ "main title"
166
+ else
167
+ node["type"]
168
+ end
169
+ hash[type] ||= []
170
+ hash[type] << node["value"]
171
+ end.compact_blank
172
+ end
173
+
174
+ # Uniform titles can have associated person names.
175
+ # @return [String, nil]
176
+ def names
177
+ Janeway.enum_for("$.note[?(@.type=='associated name')]", cocina).map do |name|
178
+ Contributors::Name.new(name).to_s(with_date: true)
179
+ end
180
+ end
181
+
182
+ # Number of nonsorting characters to ignore at the start of the title.
183
+ # @return [Integer, nil]
184
+ def nonsorting_char_count
185
+ Janeway.enum_for("$.note[?(@.type=='nonsorting character count')].value", cocina).first&.to_i || 0
186
+ end
187
+
188
+ # Type-specific label for the title, falling back to a generic "Title".
189
+ # @return [String]
190
+ def type_label
191
+ I18n.t(type&.parameterize&.underscore, scope: "cocina_display.field_label.title", default: :title)
192
+ end
193
+ end
194
+ end
@@ -21,7 +21,7 @@ module CocinaDisplay
21
21
  end.delete_suffix(delimiter)
22
22
  end
23
23
 
24
- # Recursively flatten structured, parallel, and grouped values in Cocina metadata.
24
+ # Recursively flatten structured, and grouped values in Cocina metadata.
25
25
  # Returns a list of hashes representing the "leaf" nodes with +value+ key.
26
26
  # @return [Array<Hash>] List of node hashes with "value" present
27
27
  # @param cocina [Hash] The Cocina structured data to flatten
@@ -35,8 +35,8 @@ module CocinaDisplay
35
35
  # cocina = { "structuredValue" => [{"value" => "foo"}, {"value" => "bar"}] }
36
36
  # Utils.flatten_nested_values(cocina)
37
37
  # #=> [{"value" => "foo"}, {"value" => "bar"}]
38
- # @example parallel structured and simple values
39
- # cocina = { "parallelValue" => [{"value" => "foo" }, { "structuredValue" => [{"value" => "bar"}, {"value" => "baz"}] }] }
38
+ # @example nested structured and simple values
39
+ # cocina = { "structuredValue" => [{"value" => "foo" }, { "structuredValue" => [{"value" => "bar"}, {"value" => "baz"}] }] }
40
40
  # Utils.flatten_nested_values(cocina)
41
41
  # #=> [{"value" => "foo"}, {"value" => "foo"}, {"value" => "baz"}]
42
42
  def self.flatten_nested_values(cocina, output = [], atomic_types: [])
@@ -44,7 +44,7 @@ module CocinaDisplay
44
44
  return [cocina] if atomic_types.include?(cocina["type"])
45
45
  return cocina.flat_map { |node| flatten_nested_values(node, output, atomic_types: atomic_types) } if cocina.is_a?(Array)
46
46
 
47
- nested_values = Array(cocina["structuredValue"]) + Array(cocina["parallelValue"]) + Array(cocina["groupedValue"])
47
+ nested_values = Array(cocina["structuredValue"]) + Array(cocina["groupedValue"])
48
48
  return output unless nested_values.any?
49
49
 
50
50
  nested_values.flat_map { |node| flatten_nested_values(node, output, atomic_types: atomic_types) }
@@ -2,5 +2,5 @@
2
2
 
3
3
  # :nodoc:
4
4
  module CocinaDisplay
5
- VERSION = "1.1.3" # :nodoc:
5
+ VERSION = "1.2.0" # :nodoc:
6
6
  end
@@ -1,4 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cocina_display/version"
4
- require_relative "cocina_display/cocina_record"
3
+ # require_relative "cocina_display/version"
4
+
5
+ require "janeway"
6
+ require "json"
7
+ require "net/http"
8
+ require "active_support"
9
+ require "active_support/core_ext/object/blank"
10
+ require "active_support/core_ext/hash/conversions"
11
+ require "geo/coord"
12
+ require "edtf"
13
+ require "i18n"
14
+ require "i18n/backend/fallbacks"
15
+ I18n::Backend::Simple.include I18n::Backend::Fallbacks
16
+ I18n.load_path += Dir["#{File.expand_path("..", __dir__)}/config/locales/*.yml"]
17
+ I18n.backend.load_translations
18
+
19
+ require "zeitwerk"
20
+ loader = Zeitwerk::Loader.new
21
+ loader.tag = File.basename(__FILE__, ".rb")
22
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
23
+ loader.inflector.inflect("searchworks_languages" => "SEARCHWORKS_LANGUAGES",
24
+ "marc_relator" => "MARC_RELATOR",
25
+ "marc_country" => "MARC_COUNTRY")
26
+ loader.push_dir(File.dirname(__FILE__))
27
+ loader.setup
28
+
29
+ module CocinaDisplay
30
+ # set to an object with a #notify method. This is called if an error is encountered.
31
+ mattr_accessor :notifier
32
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocina_display
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Budak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-20 00:00:00.000000000 Z
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: janeway-jsonpath
@@ -53,19 +53,19 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.2'
55
55
  - !ruby/object:Gem::Dependency
56
- name: iso639
56
+ name: i18n
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '1.3'
61
+ version: '0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '1.3'
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: geo_coord
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: zeitwerk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.7'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.7'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -210,35 +224,55 @@ files:
210
224
  - LICENSE
211
225
  - README.md
212
226
  - Rakefile
227
+ - config/i18n-tasks.yml
228
+ - config/licenses.yml
229
+ - config/locales/en.yml
213
230
  - lib/cocina_display.rb
214
231
  - lib/cocina_display/cocina_record.rb
215
- - lib/cocina_display/concerns/access.rb
232
+ - lib/cocina_display/concerns.rb
233
+ - lib/cocina_display/concerns/accesses.rb
216
234
  - lib/cocina_display/concerns/contributors.rb
217
235
  - lib/cocina_display/concerns/events.rb
218
236
  - lib/cocina_display/concerns/forms.rb
219
237
  - lib/cocina_display/concerns/geospatial.rb
220
238
  - lib/cocina_display/concerns/identifiers.rb
221
239
  - lib/cocina_display/concerns/languages.rb
240
+ - lib/cocina_display/concerns/notes.rb
241
+ - lib/cocina_display/concerns/related_resources.rb
222
242
  - lib/cocina_display/concerns/structural.rb
223
243
  - lib/cocina_display/concerns/subjects.rb
224
244
  - lib/cocina_display/concerns/titles.rb
245
+ - lib/cocina_display/concerns/url_helpers.rb
225
246
  - lib/cocina_display/contributors/contributor.rb
226
247
  - lib/cocina_display/contributors/name.rb
227
248
  - lib/cocina_display/contributors/role.rb
228
249
  - lib/cocina_display/dates/date.rb
229
250
  - lib/cocina_display/dates/date_range.rb
251
+ - lib/cocina_display/description/access.rb
252
+ - lib/cocina_display/description/access_contact.rb
253
+ - lib/cocina_display/description/url.rb
254
+ - lib/cocina_display/display_data.rb
230
255
  - lib/cocina_display/events/event.rb
231
256
  - lib/cocina_display/events/imprint.rb
232
257
  - lib/cocina_display/events/location.rb
258
+ - lib/cocina_display/events/note.rb
259
+ - lib/cocina_display/forms/form.rb
260
+ - lib/cocina_display/forms/genre.rb
261
+ - lib/cocina_display/forms/resource_type.rb
233
262
  - lib/cocina_display/geospatial.rb
263
+ - lib/cocina_display/identifier.rb
264
+ - lib/cocina_display/json_backed_record.rb
234
265
  - lib/cocina_display/language.rb
266
+ - lib/cocina_display/license.rb
267
+ - lib/cocina_display/note.rb
268
+ - lib/cocina_display/related_resource.rb
235
269
  - lib/cocina_display/subjects/subject.rb
236
270
  - lib/cocina_display/subjects/subject_value.rb
237
- - lib/cocina_display/title_builder.rb
271
+ - lib/cocina_display/title.rb
238
272
  - lib/cocina_display/utils.rb
239
273
  - lib/cocina_display/version.rb
240
- - lib/cocina_display/vocabularies/marc_country_codes.rb
241
- - lib/cocina_display/vocabularies/marc_relator_codes.rb
274
+ - lib/cocina_display/vocabularies/marc_country.rb
275
+ - lib/cocina_display/vocabularies/marc_relator.rb
242
276
  - lib/cocina_display/vocabularies/searchworks_languages.rb
243
277
  - script/deep_compact.rb
244
278
  - script/find_records.rb