cocina_display 1.5.0 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f613a3974d14946081f09d45797c2be75475c2b37fedf4ebe51a9c7eef2de10
4
- data.tar.gz: 6dc64d8348e652ac4bea87a904519029ed580cc498c6e584d2a196a079293141
3
+ metadata.gz: 46cfe681a820ad5986d0503157b5add9a8cb089af31e3afaba0ed2964d33bda6
4
+ data.tar.gz: b60af2ecc9cfdc031e57ae98cb1093aa3b111a5c14dda40b71814e07a9f2d941
5
5
  SHA512:
6
- metadata.gz: 56874880d8a3fff624afb2306aeffab7792b59f678901a5f8632bd77beca3eeaab388053ddcc3790d6f1c5dfe687ee6588cd83eafe5cc143893eb8d551ed617f
7
- data.tar.gz: d39907bb6d250a06ab01b5127fc98223ac6225e2e346ea9eaadaa91874ff20b807e2ded45453e5ed10cfb9515eba4b3da11ab5fb8ee7b42718ac670ab4cde95f
6
+ metadata.gz: 2c010edefb699c21c82a3c426befcb16e4fa0e72aeeda6e77c2ed8eba9fa1aa6e365e889fda6f94f6d118a3bde2a968e4ece610f918995721c14a1fc6fc5b723
7
+ data.tar.gz: fa61cbcd25787c2ec552fff17f5282e425ff5b46d03f0b60476c017faab41f7f059bd7630e54c2fc753a6330cf87a41c12bda20fbd3bd602e567264cfa6b133b
data/README.md CHANGED
@@ -40,7 +40,7 @@ There is also a helper method to fetch the Cocina JSON for a given DRUID over HT
40
40
  The `CocinaRecord` class provides some methods to access common fields, as well as an underlying hash representation parsed from the JSON.
41
41
 
42
42
  ```ruby
43
- > record.main_title
43
+ > record.short_title
44
44
  => "Bugatti Type 51A. Road & Track Salon January 1957"
45
45
  > record.content_type
46
46
  => "image"
@@ -51,17 +51,17 @@ module CocinaDisplay
51
51
  end
52
52
 
53
53
  # SDR content type of the object.
54
- # @note {RelatedResource}s may not have a content type.
55
54
  # @return [String, nil]
56
55
  # @see https://github.com/sul-dlss/cocina-models/blob/main/openapi.yml#L532-L546
57
56
  # @example
58
57
  # record.content_type #=> "image"
59
58
  def content_type
60
- cocina_doc["type"].delete_prefix("https://cocina.sul.stanford.edu/models/")
59
+ cocina_doc["type"]&.delete_prefix("https://cocina.sul.stanford.edu/models/")
61
60
  end
62
61
 
63
62
  # Primary processing label for the object.
64
- # @note This may or may not be the same as the title.
63
+ # @note For public Cocina fetched via PURL, this is generated at publish time from the title. It will have the same value as #display_title.
64
+ # @see https://github.com/sul-dlss/cocina_display/issues/205#issuecomment-3781393715
65
65
  # @return [String, nil]
66
66
  def label
67
67
  cocina_doc["label"]
@@ -27,6 +27,12 @@ module CocinaDisplay
27
27
  publisher_contributors.flat_map(&:display_names).compact
28
28
  end
29
29
 
30
+ # All names of authors, formatted for display.
31
+ # @return [Array<String>]
32
+ def author_names
33
+ author_contributors.flat_map(&:display_names).compact
34
+ end
35
+
30
36
  # All names of contributors who are people, formatted for display.
31
37
  # @param with_date [Boolean] Include life dates, if present
32
38
  # @return [Array<String>]
@@ -102,13 +108,18 @@ module CocinaDisplay
102
108
  end
103
109
 
104
110
  # All contributors for the object, including authors, editors, etc.
105
- # Checks both description.contributor and description.event.contributor.
111
+ # @note Does not include contributors attached to events.
106
112
  # @return [Array<Contributor>]
107
113
  def contributors
108
- @contributors ||= Enumerator::Chain.new(
109
- path("$.description.contributor.*"),
110
- path("$.description.event.*.contributor.*")
111
- ).map { |c| CocinaDisplay::Contributors::Contributor.new(c) }
114
+ @contributors ||= path("$.description.contributor.*")
115
+ .map { |c| CocinaDisplay::Contributors::Contributor.new(c) }
116
+ end
117
+
118
+ # All contributors with an "author" role.
119
+ # @return [Array<Contributor>]
120
+ # @see Contributor#author?
121
+ def author_contributors
122
+ contributors.filter(&:author?)
112
123
  end
113
124
 
114
125
  # All contributors with a "publisher" role.
@@ -129,7 +140,8 @@ module CocinaDisplay
129
140
  contributors.find(&:primary?).presence || contributors.find { |c| !c.role? }.presence || contributors.first
130
141
  end
131
142
 
132
- # All contributors except the main one.
143
+ # Contributors other than the main contributor.
144
+ # Also excludes the contributor (usually publisher) coming from an imprint event.
133
145
  # @return [Array<Contributor>]
134
146
  def additional_contributors
135
147
  return [] if contributors.empty? || contributors.one?
@@ -63,8 +63,8 @@ module CocinaDisplay
63
63
  # String for displaying the imprint statement(s).
64
64
  # @return [String, nil]
65
65
  # @see CocinaDisplay::Imprint#to_s
66
- # @example
67
- # CocinaRecord.fetch('bt553vr2845').imprint_str #=> "New York : Meridian Book, 1993, c1967"
66
+ # @example bt553vr2845
67
+ # "New York : Meridian Book, 1993, c1967"
68
68
  def imprint_str
69
69
  imprint_events.map(&:to_s).compact_blank.join("; ")
70
70
  end
@@ -73,7 +73,14 @@ module CocinaDisplay
73
73
  # Considers locations for all publication, creation, and capture events.
74
74
  # @return [Array<String>]
75
75
  def publication_places
76
- publication_events.flat_map { |event| event.locations.map(&:to_s) }
76
+ publication_events.flat_map { |event| event.locations.map(&:to_s) }.compact_blank.uniq
77
+ end
78
+
79
+ # List of countries of publication as strings.
80
+ # Considers locations for all publication, creation, and capture events.
81
+ # @return [Array<String>]
82
+ def publication_countries
83
+ publication_events.flat_map { |event| event.locations.map(&:country_name) }.compact_blank.uniq
77
84
  end
78
85
 
79
86
  # All root level events associated with the object.
@@ -75,10 +75,16 @@ module CocinaDisplay
75
75
  end
76
76
 
77
77
  # All form notes to be rendered for display.
78
+ # Checks both description.form.note and description.geographic.form.note.
78
79
  # @return [Array<DisplayData>]
79
80
  def form_note_display_data
80
- CocinaDisplay::DisplayData.from_cocina(path("$.description.form[*].note[*]"),
81
- label: I18n.t("cocina_display.field_label.form.note"))
81
+ CocinaDisplay::DisplayData.from_cocina(
82
+ Enumerator::Chain.new(
83
+ path("$.description.form.*.note.*"),
84
+ path("$.description.geographic.*.form.*.note.*")
85
+ ),
86
+ label: I18n.t("cocina_display.field_label.form.note")
87
+ )
82
88
  end
83
89
 
84
90
  # Is the object a periodical or serial?
@@ -106,10 +112,14 @@ module CocinaDisplay
106
112
  end
107
113
 
108
114
  # Collapses all nested form values into an array of {Form} objects.
115
+ # Checks both description.form and description.geographic.form.
109
116
  # Preserves resource type without flattening, since it can be structured.
110
117
  # @return [Array<Form>]
111
118
  def all_forms
112
- @all_forms ||= path("$.description.form.*")
119
+ @all_forms ||= Enumerator::Chain.new(
120
+ path("$.description.form.*"),
121
+ path("$.description.geographic.*.form.*")
122
+ )
113
123
  .flat_map { |form| Utils.flatten_nested_values(form, atomic_types: ["resource type"]) }
114
124
  .map { |form| CocinaDisplay::Forms::Form.from_cocina(form) }
115
125
  end
@@ -36,6 +36,25 @@ module CocinaDisplay
36
36
  def total_file_size_int
37
37
  files.pluck("size").sum
38
38
  end
39
+
40
+ # DRUIDs of collections this object is a member of.
41
+ # @return [Array<String>]
42
+ # @example ["sj775xm6965"]
43
+ def containing_collections
44
+ path("$.structural.isMemberOf.*").map { |druid| druid.delete_prefix("druid:") }
45
+ end
46
+
47
+ def virtual_object?
48
+ return false if path("$.structural.contains.*").any?
49
+
50
+ path("$.structural.hasMemberOrders.*.members.*").any?
51
+ end
52
+
53
+ def virtual_object_members
54
+ return [] unless virtual_object?
55
+
56
+ path("$.structural.hasMemberOrders.*.members.*").map { |druid| druid.delete_prefix("druid:") }
57
+ end
39
58
  end
40
59
  end
41
60
  end
@@ -2,11 +2,11 @@ module CocinaDisplay
2
2
  module Concerns
3
3
  # Methods for finding and formatting titles.
4
4
  module Titles
5
- # The main title for the object, without subtitle, part name, etc.
5
+ # The short title for the object, without subtitle, part name, etc.
6
6
  # If there are multiple primary titles, uses the first.
7
- # @see CocinaDisplay::Title#main_title
7
+ # @see CocinaDisplay::Title#short_title
8
8
  # @return [String, nil]
9
- def main_title
9
+ def short_title
10
10
  primary_title&.short_title
11
11
  end
12
12
 
@@ -3,7 +3,7 @@ module CocinaDisplay
3
3
  # Methods that generate URLs to access an object.
4
4
  module UrlHelpers
5
5
  # The PURL URL for this object.
6
- # @return [String]
6
+ # @return [String, nil]
7
7
  # @example
8
8
  # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
9
9
  def purl_url
@@ -13,8 +13,7 @@ module CocinaDisplay
13
13
  # The oEmbed URL for the object, optionally with additional parameters.
14
14
  # Corresponds to the PURL environment.
15
15
  # @param params [Hash] Additional parameters to include in the oEmbed URL.
16
- # @return [String]
17
- # @return [nil] if the object is a collection.
16
+ # @return [String, nil]
18
17
  # @example Generate an oEmbed URL for the viewer and hide the title
19
18
  # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
20
19
  def oembed_url(params: {})
@@ -26,17 +25,18 @@ module CocinaDisplay
26
25
 
27
26
  # The download URL to get the entire object as a .zip file.
28
27
  # Stacks generates the .zip for the object on request.
29
- # @return [String]
28
+ # @note Collections and related resources do not have a download URL.
29
+ # @return [String, nil]
30
30
  # @example
31
31
  # record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
32
32
  def download_url
33
- "#{stacks_base_url}/object/#{bare_druid}" if bare_druid.present?
33
+ "#{stacks_base_url}/object/#{bare_druid}" if is_a?(CocinaDisplay::CocinaRecord) && bare_druid.present? && !collection?
34
34
  end
35
35
 
36
36
  # The IIIF manifest URL for the object.
37
37
  # PURL generates the IIIF manifest.
38
38
  # @param version [Integer] The IIIF presentation spec version to use (3 or 2).
39
- # @return [String]
39
+ # @return [String, nil]
40
40
  # @example
41
41
  # record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
42
42
  def iiif_manifest_url(version: 3)
@@ -44,11 +44,22 @@ module CocinaDisplay
44
44
  "#{purl_url}/#{iiif_path}/manifest" if purl_url.present?
45
45
  end
46
46
 
47
+ # The Searchworks URL for this object.
48
+ # Uses the catkey (FOLIO HRID) if present, otherwise uses the druid.
49
+ # @note This does not guarantee that the object is actually in Searchworks.
50
+ # @return [String, nil]
51
+ # @example
52
+ # record.searchworks_url #=> "https://searchworks.stanford.edu/view/bx658jh7339"
53
+ def searchworks_url
54
+ return "#{searchworks_base_url}/view/#{folio_hrid}" if folio_hrid.present?
55
+ "#{searchworks_base_url}/view/#{bare_druid}" if bare_druid.present?
56
+ end
57
+
47
58
  private
48
59
 
49
60
  # The URL to the PURL environment this object is from.
50
61
  # @note Objects accessed via UAT will still have a production PURL base URL.
51
- # @return [String]
62
+ # @return [String, nil]
52
63
  # @example
53
64
  # record.purl_base_url #=> "https://purl.stanford.edu"
54
65
  def purl_base_url
@@ -58,7 +69,7 @@ module CocinaDisplay
58
69
  # The URL to the stacks environment this object is shelved in.
59
70
  # Corresponds to the PURL environment.
60
71
  # @see purl_base_url
61
- # @return [String]
72
+ # @return [String, nil]
62
73
  # @example
63
74
  # record.stacks_base_url #=> "https://stacks.stanford.edu"
64
75
  def stacks_base_url
@@ -68,6 +79,20 @@ module CocinaDisplay
68
79
  "https://stacks.stanford.edu"
69
80
  end
70
81
  end
82
+
83
+ # The URL to the Searchworks environment this object could be found in.
84
+ # Corresponds to the PURL environment.
85
+ # @see purl_base_url
86
+ # @return [String, nil]
87
+ # @example
88
+ # record.searchworks_base_url #=> "https://searchworks.stanford.edu"
89
+ def searchworks_base_url
90
+ if purl_base_url == "https://sul-purl-stage.stanford.edu"
91
+ "https://searchworks-stage.stanford.edu"
92
+ elsif purl_base_url.present?
93
+ "https://searchworks.stanford.edu"
94
+ end
95
+ end
71
96
  end
72
97
  end
73
98
  end
@@ -72,6 +72,13 @@ module CocinaDisplay
72
72
  roles.any?
73
73
  end
74
74
 
75
+ # String representation of the contributor's role(s).
76
+ # @return [String, nil]
77
+ # @example "author, editor, publisher"
78
+ def display_role
79
+ roles.map(&:to_s).join(", ") if role?
80
+ end
81
+
75
82
  # The primary display name for the contributor as a string.
76
83
  # @param with_date [Boolean] Include life dates, if present
77
84
  # @return [String, nil]
@@ -22,7 +22,7 @@ module CocinaDisplay
22
22
  # Decodes a MARC country code if present and no value was present.
23
23
  # @return [String, nil]
24
24
  def to_s
25
- cocina["value"] || decoded_country
25
+ cocina["value"] || country_name
26
26
  end
27
27
 
28
28
  # Is there an unencoded value (name) for this location?
@@ -31,6 +31,12 @@ module CocinaDisplay
31
31
  cocina["value"].present?
32
32
  end
33
33
 
34
+ # Decoded country name if the location is encoded with a MARC country code.
35
+ # @return [String, nil]
36
+ def country_name
37
+ Location.marc_countries[code] if marc_country? && valid_country_code?
38
+ end
39
+
34
40
  private
35
41
 
36
42
  # A code, like a MARC country code, representing the location.
@@ -39,12 +45,6 @@ module CocinaDisplay
39
45
  cocina["code"]
40
46
  end
41
47
 
42
- # Decoded country name if the location is encoded with a MARC country code.
43
- # @return [String, nil]
44
- def decoded_country
45
- Location.marc_countries[code] if marc_country? && valid_country_code?
46
- end
47
-
48
48
  # Is this a decodable country code?
49
49
  # Excludes blank values and "xx" (unknown) and "vp" (various places).
50
50
  # @return [Boolean]
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CocinaDisplay
4
+ # Superclass for metadata records backed by a Cocina JSON document.
4
5
  class JsonBackedRecord
5
6
  # The parsed Cocina document.
6
7
  # @return [Hash]
7
8
  attr_reader :cocina_doc
8
9
 
9
- # Initialize a CocinaRecord with a Cocina document hash.
10
+ # Initialize a record with a Cocina document hash.
10
11
  # @param cocina_doc [Hash]
11
12
  def initialize(cocina_doc)
12
13
  @cocina_doc = cocina_doc
@@ -23,5 +24,22 @@ module CocinaDisplay
23
24
  def path(path_expression)
24
25
  Janeway.enum_for(path_expression, cocina_doc)
25
26
  end
27
+
28
+ # Flattened, normalized aggregation of all node texts in the Cocina document.
29
+ # @note Used for 'all search' fields in indexing.
30
+ # @return [String]
31
+ def text
32
+ node_texts.compact.join(" ").gsub(/\s+/, " ").strip
33
+ end
34
+
35
+ private
36
+
37
+ # Array of all node values/codes except those under "source" keys.
38
+ # Used to build flattened text representation.
39
+ # @note Source values are omitted because they usually indicate ontologies/vocabularies.
40
+ # @return [Array<String>]
41
+ def node_texts
42
+ path("$..['code', 'value']").map { |node, _p, _i, path| node unless path.to_s.include?("['source']") }
43
+ end
26
44
  end
27
45
  end
@@ -61,15 +61,14 @@ module CocinaDisplay
61
61
  # The long form of the title, including subtitle, part name, etc.
62
62
  # @note This corresponds to the entire MARC 245 field.
63
63
  # @return [String, nil]
64
- # @example "M. de Courville [estampe]"
64
+ # @example "M. de Courville : [estampe]"
65
65
  def full_title
66
66
  full_title_str.presence || cocina["value"]
67
67
  end
68
68
 
69
- # The long form of the title, with added punctuation between parts if not present.
69
+ # The long form of the title, without trailing punctuation.
70
70
  # @note This corresponds to the entire MARC 245 field.
71
71
  # @return [String, nil]
72
- # @example "M. de Courville : [estampe]"
73
72
  def display_title
74
73
  display_title_str.presence || cocina["value"]
75
74
  end
@@ -81,7 +80,7 @@ module CocinaDisplay
81
80
  def sort_title
82
81
  return "\u{10FFFF}" unless full_title
83
82
 
84
- sort_title_str
83
+ full_title[nonsorting_chars_str.length..]
85
84
  .unicode_normalize(:nfd) # Prevent accents being stripped
86
85
  .gsub(/[[:punct:]]*/, "")
87
86
  .gsub(/\W{2,}/, " ") # Collapse whitespace after removing punctuation
@@ -90,37 +89,33 @@ module CocinaDisplay
90
89
 
91
90
  private
92
91
 
93
- # Generate the short title by joining main title and nonsorting characters with spaces.
92
+ # Generate the short title by joining main title and nonsorting characters.
94
93
  # @return [String, nil]
95
94
  def short_title_str
96
- Utils.compact_and_join([nonsorting_chars_str, main_title_str])
95
+ nonsorting_chars_str + main_title_str # pre-formatted padding
97
96
  end
98
97
 
99
- # Generate the full title by joining all title components with spaces.
98
+ # Generate the full title by joining all title components with punctuation.
100
99
  # @return [String, nil]
101
100
  def full_title_str
102
- nonsorting_chars_str + sort_title_str
101
+ title_str = main_subtitle_str
102
+ title_str = Utils.compact_and_join([main_subtitle_str, parts_str], delimiter: ". ") unless main_subtitle_str.end_with?(parts_str)
103
+ title_str = nonsorting_chars_str + title_str # pre-formatted padding
104
+ title_str = Utils.compact_and_join([names_str, title_str], delimiter: ". ") if names_str.present?
105
+ title_str += "." unless title_str&.match?(/[[:punct:]]\z/)
106
+ title_str.presence
103
107
  end
104
108
 
105
- # All of the sorting parts of the title joined together with spaces.
106
- # @return [String]
107
- def sort_title_str
108
- Utils.compact_and_join([main_title_str, subtitle_str, parts_str])
109
+ # Generate the display title by stripping trailing punctuation from the full title.
110
+ # @return [String, nil]
111
+ def display_title_str
112
+ full_title_str&.sub(/[\.,;:\/\\]+\z/, "")
109
113
  end
110
114
 
111
- # Generate the display title by joining all components with punctuation:
112
- # - Join main title and subtitle with " : "
113
- # - Join part name/number/label with ", "
114
- # - Join part string with preceding title with ". "
115
- # - Prepend preformatted nonsorting characters
116
- # - Prepend associated names with ". "
115
+ # The main title and subtitle, joined together with a colon.
117
116
  # @return [String, nil]
118
- def display_title_str
119
- title_str = Utils.compact_and_join([main_title_str, subtitle_str], delimiter: " : ")
120
- title_str = Utils.compact_and_join([title_str, parts_str(delimiter: ", ")], delimiter: ". ")
121
- title_str = nonsorting_chars_str + title_str # pre-formatted padding
122
- title_str = Utils.compact_and_join([names_str, title_str], delimiter: ". ") if names_str.present?
123
- title_str.presence
117
+ def main_subtitle_str
118
+ Utils.compact_and_join([main_title_str, subtitle_str], delimiter: " : ")
124
119
  end
125
120
 
126
121
  # All nonsorting characters joined together with padding applied.
@@ -142,15 +137,14 @@ module CocinaDisplay
142
137
  Utils.compact_and_join(Array(title_components["subtitle"]))
143
138
  end
144
139
 
145
- # The part name, number, and label components, joined together.
146
- # Default delimiter is a space, but can be overridden.
140
+ # The part name, number, and label components, joined together with commas.
147
141
  # @return [String, nil]
148
- def parts_str(delimiter: " ")
142
+ def parts_str
149
143
  Utils.compact_and_join(
150
144
  Array(title_components["part number"] || @part_numbers) +
151
145
  Array(title_components["part name"]) +
152
146
  [@part_label],
153
- delimiter: delimiter
147
+ delimiter: ", "
154
148
  )
155
149
  end
156
150
 
@@ -198,8 +192,6 @@ module CocinaDisplay
198
192
  I18n.t(type&.parameterize&.underscore, scope: "cocina_display.field_label.title", default: :title)
199
193
  end
200
194
 
201
- private
202
-
203
195
  # Add or remove padding from nonsorting portion of the title.
204
196
  # @param value [String]
205
197
  # @return [String]
@@ -2,5 +2,5 @@
2
2
 
3
3
  # :nodoc:
4
4
  module CocinaDisplay
5
- VERSION = "1.5.0" # :nodoc:
5
+ VERSION = "1.7.0" # :nodoc:
6
6
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocina_display
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Budak
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-12-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: janeway-jsonpath
@@ -296,7 +296,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
296
296
  - !ruby/object:Gem::Version
297
297
  version: '0'
298
298
  requirements: []
299
- rubygems_version: 3.6.2
299
+ rubygems_version: 4.0.4
300
300
  specification_version: 4
301
301
  summary: Helpers for rendering Cocina metadata
302
302
  test_files: []