cocina_display 0.3.0 → 0.4.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 +4 -4
- data/README.md +1 -1
- data/lib/cocina_display/cocina_record.rb +6 -77
- data/lib/cocina_display/concerns/contributors.rb +94 -0
- data/lib/cocina_display/concerns/identifiers.rb +60 -0
- data/lib/cocina_display/concerns/titles.rb +64 -0
- data/lib/cocina_display/contributor.rb +179 -0
- data/lib/cocina_display/imprint.rb +9 -25
- data/lib/cocina_display/title_builder.rb +27 -7
- data/lib/cocina_display/utils.rb +33 -0
- data/lib/cocina_display/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0724f307088c9cdd9348af463e3c659811eef65d0f1f04e094b4c664a36cfed1
|
4
|
+
data.tar.gz: 4b49472d07a085455ca72204e6ebde3cfc27c634f8ba9cd5b2c2a47d62d19aad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edc1e8977792d21fd6fef292f486c11eab4011dc63c854af1217aced6930ea55538e158abc261491c08b1f8492c374c52102b44931849b2350e1dacf54310d41
|
7
|
+
data.tar.gz: 664691764d4a4ff7da024af64ce9dd2d0841d8c014ee9439c3ebebe4b3a4c2e585e7f608f2ff943a04586dfaad8fc4ec7cfa714fade929ca9b0278ec740bff8b
|
data/README.md
CHANGED
@@ -40,7 +40,7 @@ There is also a helper method to fetch the Cocina JSON for a given DRUID and imm
|
|
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.
|
43
|
+
> record.main_title
|
44
44
|
=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
45
45
|
> record.content_type
|
46
46
|
=> "image"
|
@@ -7,13 +7,18 @@ require "active_support"
|
|
7
7
|
require "active_support/core_ext/object/blank"
|
8
8
|
require "active_support/core_ext/hash/conversions"
|
9
9
|
|
10
|
-
require_relative "title_builder"
|
11
10
|
require_relative "concerns/events"
|
11
|
+
require_relative "concerns/contributors"
|
12
|
+
require_relative "concerns/identifiers"
|
13
|
+
require_relative "concerns/titles"
|
12
14
|
|
13
15
|
module CocinaDisplay
|
14
16
|
# Public Cocina metadata for an SDR object, as fetched from PURL.
|
15
17
|
class CocinaRecord
|
16
18
|
include CocinaDisplay::Concerns::Events
|
19
|
+
include CocinaDisplay::Concerns::Contributors
|
20
|
+
include CocinaDisplay::Concerns::Identifiers
|
21
|
+
include CocinaDisplay::Concerns::Titles
|
17
22
|
|
18
23
|
# Fetch a public Cocina document from PURL and create a CocinaRecord.
|
19
24
|
# @note This is intended to be used in development or testing only.
|
@@ -45,60 +50,6 @@ module CocinaDisplay
|
|
45
50
|
Janeway.enum_for(path_expression, cocina_doc)
|
46
51
|
end
|
47
52
|
|
48
|
-
# The DRUID for the object, with the +druid:+ prefix.
|
49
|
-
# @return [String]
|
50
|
-
# @example
|
51
|
-
# record.druid #=> "druid:bb099mt5053"
|
52
|
-
def druid
|
53
|
-
cocina_doc["externalIdentifier"]
|
54
|
-
end
|
55
|
-
|
56
|
-
# The DRUID for the object, without the +druid:+ prefix.
|
57
|
-
# @return [String]
|
58
|
-
# @example
|
59
|
-
# record.bare_druid #=> "bb099mt5053"
|
60
|
-
def bare_druid
|
61
|
-
druid.delete_prefix("druid:")
|
62
|
-
end
|
63
|
-
|
64
|
-
# The DOI for the object, if there is one – just the identifier part.
|
65
|
-
# @return [String, nil]
|
66
|
-
# @example
|
67
|
-
# record.doi #=> "10.25740/ppax-bf07"
|
68
|
-
def doi
|
69
|
-
doi_id = path("$.identification.doi").first ||
|
70
|
-
path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
|
71
|
-
path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
|
72
|
-
|
73
|
-
URI(doi_id).path.delete_prefix("/") if doi_id.present?
|
74
|
-
end
|
75
|
-
|
76
|
-
# The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
|
77
|
-
# @return [String, nil]
|
78
|
-
# @example
|
79
|
-
# record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
|
80
|
-
def doi_url
|
81
|
-
URI.join("https://doi.org", doi).to_s if doi.present?
|
82
|
-
end
|
83
|
-
|
84
|
-
# The HRID of the item in FOLIO, if defined.
|
85
|
-
# @note This doesn't imply the object is available in Searchworks at this ID.
|
86
|
-
# @return [String, nil]
|
87
|
-
# @example
|
88
|
-
# record.folio_hrid #=> "a12845814"
|
89
|
-
def folio_hrid
|
90
|
-
path("$.identification.catalogLinks[?(@.catalog == 'folio')].catalogRecordId").first
|
91
|
-
end
|
92
|
-
|
93
|
-
# The FOLIO HRID if defined, otherwise the bare DRUID.
|
94
|
-
# @note This doesn't imply the object is available in Searchworks at this ID.
|
95
|
-
# @see folio_hrid
|
96
|
-
# @see bare_druid
|
97
|
-
# @return [String]
|
98
|
-
def searchworks_id
|
99
|
-
folio_hrid || bare_druid
|
100
|
-
end
|
101
|
-
|
102
53
|
# Timestamp when the Cocina was created.
|
103
54
|
# @note This is for the metadata itself, not the object.
|
104
55
|
# @return [Time]
|
@@ -128,28 +79,6 @@ module CocinaDisplay
|
|
128
79
|
content_type == "collection"
|
129
80
|
end
|
130
81
|
|
131
|
-
# The main title for the object.
|
132
|
-
# @note If you need more formatting control, consider using {CocinaDisplay::TitleBuilder} directly.
|
133
|
-
# @return [String]
|
134
|
-
# @example
|
135
|
-
# record.title #=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
136
|
-
def title
|
137
|
-
CocinaDisplay::TitleBuilder.build(
|
138
|
-
cocina_doc.dig("description", "title"),
|
139
|
-
catalog_links: cocina_doc.dig("identification", "catalogLinks")
|
140
|
-
)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Alternative or translated titles for the object. Does not include the main title.
|
144
|
-
# @return [Array<String>]
|
145
|
-
# @example
|
146
|
-
# record.additional_titles #=> ["Alternate title 1", "Alternate title 2"]
|
147
|
-
def additional_titles
|
148
|
-
CocinaDisplay::TitleBuilder.additional_titles(
|
149
|
-
cocina_doc.dig("description", "title")
|
150
|
-
)
|
151
|
-
end
|
152
|
-
|
153
82
|
# Traverse nested FileSets and return an enumerator over their files.
|
154
83
|
# Each file is a +Hash+.
|
155
84
|
# @return [Enumerator] Enumerator over file hashes
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative "../contributor"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for finding and formatting names for contributors
|
6
|
+
module Contributors
|
7
|
+
# The main author's name, formatted for display.
|
8
|
+
# @param with_date [Boolean] Include life dates, if present
|
9
|
+
# @return [String]
|
10
|
+
# @return [nil] if no main author is found
|
11
|
+
# @example
|
12
|
+
# record.main_author #=> "Smith, John"
|
13
|
+
# @example with date
|
14
|
+
# record.main_author(with_date: true) #=> "Smith, John, 1970-2020"
|
15
|
+
def main_author(with_date: false)
|
16
|
+
main_author_contributor&.display_name(with_date: with_date)
|
17
|
+
end
|
18
|
+
|
19
|
+
# All author names except the main one, formatted for display.
|
20
|
+
# @param with_date [Boolean] Include life dates, if present
|
21
|
+
# @return [Array<String>]
|
22
|
+
def additional_authors(with_date: false)
|
23
|
+
additional_author_contributors.map { |c| c.display_name(with_date: with_date) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# All names of authors who are people, formatted for display.
|
27
|
+
# @param with_date [Boolean] Include life dates, if present
|
28
|
+
# @return [Array<String>]
|
29
|
+
def person_authors(with_date: false)
|
30
|
+
authors.filter(&:person?).map { |c| c.display_name(with_date: with_date) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# All names of non-person authors, formatted for display.
|
34
|
+
# This includes organizations, conferences, families, etc.
|
35
|
+
# @return [Array<String>]
|
36
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-types
|
37
|
+
def impersonal_authors
|
38
|
+
authors.reject(&:person?).map(&:display_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# All names of authors that are organizations, formatted for display.
|
42
|
+
# @return [Array<String>]
|
43
|
+
def organization_authors
|
44
|
+
authors.filter(&:organization?).map(&:display_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# All names of authors that are conferences, formatted for display.
|
48
|
+
# @return [Array<String>]
|
49
|
+
def conference_authors
|
50
|
+
authors.filter(&:conference?).map(&:display_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# A string value for sorting by author that sorts missing values last.
|
54
|
+
# Ignores punctuation and leading/trailing spaces.
|
55
|
+
# @return [String]
|
56
|
+
def sort_author
|
57
|
+
(main_author_contributor&.display_name || "\u{10FFFF}").gsub(/[[:punct:]]*/, "").strip
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# All contributors for the object, including authors, editors, etc.
|
63
|
+
# @return [Array<Contributor>]
|
64
|
+
def contributors
|
65
|
+
@contributors ||= path("$.description.contributor[*]").map { |c| Contributor.new(c) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# All contributors with a "creator" or "author" role.
|
69
|
+
# @return [Array<Contributor>]
|
70
|
+
# @see Contributor#author?
|
71
|
+
def authors
|
72
|
+
contributors.filter(&:author?)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Contributor object representing the primary author.
|
76
|
+
# Selected according to the following rules:
|
77
|
+
# 1. If there is a primary author or creator, use that.
|
78
|
+
# 2. If there are no primary authors or creators, use the first one.
|
79
|
+
# 3. If there are none at all, use the first contributor without any role.
|
80
|
+
# @return [Contributor]
|
81
|
+
# @return [nil] if no suitable contributor is found
|
82
|
+
def main_author_contributor
|
83
|
+
authors.find(&:primary?).presence || authors.first || contributors.find { |c| !c.role? }.presence
|
84
|
+
end
|
85
|
+
|
86
|
+
# All author/creator contributors except the main one.
|
87
|
+
# @return [Array<Contributor>]
|
88
|
+
def additional_author_contributors
|
89
|
+
return [] if authors.empty? || authors.one? || !authors.include?(main_author_contributor)
|
90
|
+
authors - [main_author_contributor]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
module Concerns
|
3
|
+
# Methods for extracting and formatting identifiers from Cocina records.
|
4
|
+
module Identifiers
|
5
|
+
# The DRUID for the object, with the +druid:+ prefix.
|
6
|
+
# @return [String]
|
7
|
+
# @example
|
8
|
+
# record.druid #=> "druid:bb099mt5053"
|
9
|
+
def druid
|
10
|
+
cocina_doc["externalIdentifier"]
|
11
|
+
end
|
12
|
+
|
13
|
+
# The DRUID for the object, without the +druid:+ prefix.
|
14
|
+
# @return [String]
|
15
|
+
# @example
|
16
|
+
# record.bare_druid #=> "bb099mt5053"
|
17
|
+
def bare_druid
|
18
|
+
druid.delete_prefix("druid:")
|
19
|
+
end
|
20
|
+
|
21
|
+
# The DOI for the object, if there is one – just the identifier part.
|
22
|
+
# @return [String, nil]
|
23
|
+
# @example
|
24
|
+
# record.doi #=> "10.25740/ppax-bf07"
|
25
|
+
def doi
|
26
|
+
doi_id = path("$.identification.doi").first ||
|
27
|
+
path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
|
28
|
+
path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
|
29
|
+
|
30
|
+
URI(doi_id).path.delete_prefix("/") if doi_id.present?
|
31
|
+
end
|
32
|
+
|
33
|
+
# The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
|
34
|
+
# @return [String, nil]
|
35
|
+
# @example
|
36
|
+
# record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
|
37
|
+
def doi_url
|
38
|
+
URI.join("https://doi.org", doi).to_s if doi.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
# The HRID of the item in FOLIO, if defined.
|
42
|
+
# @note This doesn't imply the object is available in Searchworks at this ID.
|
43
|
+
# @return [String, nil]
|
44
|
+
# @example
|
45
|
+
# record.folio_hrid #=> "a12845814"
|
46
|
+
def folio_hrid
|
47
|
+
path("$.identification.catalogLinks[?(@.catalog == 'folio')].catalogRecordId").first
|
48
|
+
end
|
49
|
+
|
50
|
+
# The FOLIO HRID if defined, otherwise the bare DRUID.
|
51
|
+
# @note This doesn't imply the object is available in Searchworks at this ID.
|
52
|
+
# @see folio_hrid
|
53
|
+
# @see bare_druid
|
54
|
+
# @return [String]
|
55
|
+
def searchworks_id
|
56
|
+
folio_hrid || bare_druid
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative "../title_builder"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for finding and formatting titles.
|
6
|
+
module Titles
|
7
|
+
# The main title for the object, without subtitle, part name, etc.
|
8
|
+
# If there are multiple titles, uses the first.
|
9
|
+
# @see CocinaDisplay::TitleBuilder#main_title
|
10
|
+
# @note This corresponds to the "short title" in MODS XML, or MARC 245$a only.
|
11
|
+
# @return [String]
|
12
|
+
def main_title
|
13
|
+
CocinaDisplay::TitleBuilder.main_title(cocina_titles).first
|
14
|
+
end
|
15
|
+
|
16
|
+
# The full title for the object, including subtitle, part name, etc.
|
17
|
+
# If there are multiple titles, uses the first.
|
18
|
+
# @see CocinaDisplay::TitleBuilder#full_title
|
19
|
+
# @note This corresponds to the entire MARC 245 field.
|
20
|
+
# @return [String]
|
21
|
+
def full_title
|
22
|
+
CocinaDisplay::TitleBuilder.full_title(cocina_titles, catalog_links: catalog_links).first
|
23
|
+
end
|
24
|
+
|
25
|
+
# The full title, joined together with additional punctuation.
|
26
|
+
# If there are multiple titles, uses the first.
|
27
|
+
# @see CocinaDisplay::TitleBuilder#build
|
28
|
+
# @return [String]
|
29
|
+
def display_title
|
30
|
+
CocinaDisplay::TitleBuilder.build(cocina_titles, catalog_links: catalog_links)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Any additional titles for the object excluding the main title.
|
34
|
+
# @return [Array<String>]
|
35
|
+
# @see CocinaDisplay::TitleBuilder#additional_titles
|
36
|
+
def additional_titles
|
37
|
+
CocinaDisplay::TitleBuilder.additional_titles(cocina_titles)
|
38
|
+
end
|
39
|
+
|
40
|
+
# A string value for sorting by title that sorts missing values last.
|
41
|
+
# Ignores punctuation, leading/trailing spaces, and non-sorting characters.
|
42
|
+
# @see CocinaDisplay::TitleBuilder#sort_title
|
43
|
+
# @return [String]
|
44
|
+
def sort_title
|
45
|
+
CocinaDisplay::TitleBuilder.sort_title(cocina_titles, catalog_links: catalog_links).first || "\u{10FFFF}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# The titles from the Cocina document, as an array of hashes.
|
51
|
+
# @return [Array<Hash>]
|
52
|
+
def cocina_titles
|
53
|
+
@cocina_titles ||= Array(cocina_doc.dig("description", "title"))
|
54
|
+
end
|
55
|
+
|
56
|
+
# The catalog links from the Cocina document, as an array of hashes.
|
57
|
+
# These link to FOLIO and can include part labels used to construct titles.
|
58
|
+
# @return [Array<Hash>]
|
59
|
+
def catalog_links
|
60
|
+
@catalog_links ||= Array(cocina_doc.dig("identification", "catalogLinks"))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/array/conversions"
|
6
|
+
|
7
|
+
require_relative "utils"
|
8
|
+
|
9
|
+
module CocinaDisplay
|
10
|
+
# A contributor to a work, such as an author or publisher.
|
11
|
+
class Contributor
|
12
|
+
attr_reader :cocina
|
13
|
+
|
14
|
+
# Initialize a Contributor object with Cocina structured data.
|
15
|
+
# @param cocina [Hash] The Cocina structured data for the contributor.
|
16
|
+
def initialize(cocina)
|
17
|
+
@cocina = cocina
|
18
|
+
end
|
19
|
+
|
20
|
+
# String representation of the contributor, including name and role.
|
21
|
+
# Used for debugging and logging.
|
22
|
+
# @return [String]
|
23
|
+
def to_s
|
24
|
+
Utils.compact_and_join([display_name, display_role], delimiter: ": ")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Is this contributor a human?
|
28
|
+
# @return [Boolean]
|
29
|
+
def person?
|
30
|
+
cocina["type"] == "person"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Is this contributor an organization?
|
34
|
+
# @return [Boolean]
|
35
|
+
def organization?
|
36
|
+
cocina["type"] == "organization"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is this contributor a conference?
|
40
|
+
# @return [Boolean]
|
41
|
+
def conference?
|
42
|
+
cocina["type"] == "conference"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Is this contributor marked as primary?
|
46
|
+
# @return [Boolean]
|
47
|
+
def primary?
|
48
|
+
cocina["status"] == "primary"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Does this contributor have a role that indicates they are an author?
|
52
|
+
# @return [Boolean]
|
53
|
+
def author?
|
54
|
+
roles.any? { |role| role["value"] =~ /(author|creator)/i }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Does this contributor have any roles defined?
|
58
|
+
# @return [Boolean]
|
59
|
+
def role?
|
60
|
+
roles.any?
|
61
|
+
end
|
62
|
+
|
63
|
+
# The display name for the contributor as a string.
|
64
|
+
# Uses the first name if multiple names are present.
|
65
|
+
# @param with_date [Boolean] Include life dates, if present
|
66
|
+
# @return [String]
|
67
|
+
def display_name(with_date: false)
|
68
|
+
names.map { |name| name.display_str(with_date: with_date) }.first
|
69
|
+
end
|
70
|
+
|
71
|
+
# A string representation of the contributor's roles, formatted for display.
|
72
|
+
# If there are multiple roles, they are joined with commas.
|
73
|
+
# @return [String]
|
74
|
+
def display_role
|
75
|
+
roles.map { |role| role["value"] }.to_sentence
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# All names in the Cocina as Name objects.
|
81
|
+
# @return [Array<Name>]
|
82
|
+
def names
|
83
|
+
@names ||= Array(cocina["name"]).map { |name| Name.new(name) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# All roles in the Cocina structured data.
|
87
|
+
# @return [Array<Hash>]
|
88
|
+
def roles
|
89
|
+
Array(cocina["role"])
|
90
|
+
end
|
91
|
+
|
92
|
+
# A name associated with a contributor.
|
93
|
+
class Name
|
94
|
+
attr_reader :cocina
|
95
|
+
|
96
|
+
# Initialize a Name object with Cocina structured data.
|
97
|
+
# @param cocina [Hash] The Cocina structured data for the name.
|
98
|
+
def initialize(cocina)
|
99
|
+
@cocina = cocina
|
100
|
+
end
|
101
|
+
|
102
|
+
# The display string for the name, optionally including life dates.
|
103
|
+
# @param with_date [Boolean] Include life dates, if present
|
104
|
+
# @return [String]
|
105
|
+
# @example no dates
|
106
|
+
# name.display_name # => "King, Martin Luther, Jr."
|
107
|
+
# @example with dates
|
108
|
+
# name.display_name(with_date: true) # => "King, Martin Luther, Jr., 1929-1968"
|
109
|
+
def display_str(with_date: false)
|
110
|
+
if dates_str.present? && with_date
|
111
|
+
Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
|
112
|
+
else
|
113
|
+
full_name_str
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# The full name as a string.
|
120
|
+
# If any names were marked as "display", prefer those.
|
121
|
+
# Otherwise, combine all name components.
|
122
|
+
# @return [String]
|
123
|
+
def full_name_str
|
124
|
+
display_name_str.presence || Utils.compact_and_join(name_components, delimiter: ", ")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Flattened form of any names explicitly marked as "display name".
|
128
|
+
# @return [String]
|
129
|
+
def display_name_str
|
130
|
+
Utils.compact_and_join(Array(name_values["display"]), delimiter: ", ")
|
131
|
+
end
|
132
|
+
|
133
|
+
# List of all name components.
|
134
|
+
# If any of forename, surname, or term of address are present, those are used.
|
135
|
+
# Otherwise, fall back to any names explicitly marked as "name" or untyped.
|
136
|
+
# @return [Array<String>]
|
137
|
+
def name_components
|
138
|
+
[surname_str, forename_ordinal_str, terms_of_address_str].compact_blank.presence || Array(name_values["name"])
|
139
|
+
end
|
140
|
+
|
141
|
+
# Flatten all forenames and ordinals into a single string.
|
142
|
+
# @return [String]
|
143
|
+
def forename_ordinal_str
|
144
|
+
Utils.compact_and_join(Array(name_values["forename"]) + Array(name_values["ordinal"]), delimiter: " ")
|
145
|
+
end
|
146
|
+
|
147
|
+
# Flatten all terms of address into a single string.
|
148
|
+
# @return [String]
|
149
|
+
def terms_of_address_str
|
150
|
+
Utils.compact_and_join(Array(name_values["term of address"]), delimiter: ", ")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Flatten all surnames into a single string.
|
154
|
+
# @return [String]
|
155
|
+
def surname_str
|
156
|
+
Utils.compact_and_join(Array(name_values["surname"]), delimiter: " ")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Flatten all life and activity dates into a single string.
|
160
|
+
# @return [String]
|
161
|
+
def dates_str
|
162
|
+
Utils.compact_and_join(Array(name_values["life dates"]) + Array(name_values["activity dates"]), delimiter: ", ")
|
163
|
+
end
|
164
|
+
|
165
|
+
# A hash mapping destructured name types to their values.
|
166
|
+
# Name values with no type are grouped under "name".
|
167
|
+
# @return [Hash<String, Array<String>>]
|
168
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-name-part-types-for-structured-value
|
169
|
+
# @note Currently we do nothing with "alternative", "inverted full name", "pseudonym", and "transliteration" types.
|
170
|
+
def name_values
|
171
|
+
Utils.flatten_structured_values(cocina).each_with_object({}) do |node, hash|
|
172
|
+
type = node["type"] || "name"
|
173
|
+
hash[type] ||= []
|
174
|
+
hash[type] << node["value"]
|
175
|
+
end.compact_blank
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -5,9 +5,10 @@ require "active_support"
|
|
5
5
|
require "active_support/core_ext/enumerable"
|
6
6
|
require "active_support/core_ext/object/blank"
|
7
7
|
|
8
|
+
require_relative "utils"
|
9
|
+
require_relative "marc_country_codes"
|
8
10
|
require_relative "dates/date"
|
9
11
|
require_relative "dates/date_range"
|
10
|
-
require_relative "marc_country_codes"
|
11
12
|
|
12
13
|
module CocinaDisplay
|
13
14
|
# Wrapper for Cocina events used to generate an imprint statement for display.
|
@@ -20,23 +21,6 @@ module CocinaDisplay
|
|
20
21
|
cocina_dates.map { |cd| CocinaDisplay::Dates::Date.from_cocina(cd) }.filter(&:parsable?).compact
|
21
22
|
end
|
22
23
|
|
23
|
-
# Join non-empty values into a string using provided delimiter.
|
24
|
-
# If values already end in delimiter (ignoring whitespace), join with a space instead.
|
25
|
-
# @param values [Array<String>] The values to compact and join
|
26
|
-
# @param delimiter [String] The delimiter to use for joining, default is space
|
27
|
-
def self.compact_and_join(values, delimiter: " ")
|
28
|
-
compacted_values = values.compact_blank.map(&:strip)
|
29
|
-
return compacted_values.first if compacted_values.one?
|
30
|
-
|
31
|
-
compacted_values.reduce(+"") do |result, value|
|
32
|
-
result << if value.end_with?(delimiter.strip)
|
33
|
-
value + " "
|
34
|
-
else
|
35
|
-
value + delimiter
|
36
|
-
end
|
37
|
-
end.delete_suffix(delimiter)
|
38
|
-
end
|
39
|
-
|
40
24
|
attr_reader :cocina, :dates
|
41
25
|
|
42
26
|
# Initialize the imprint with Cocina event data.
|
@@ -49,9 +33,9 @@ module CocinaDisplay
|
|
49
33
|
# The entire imprint statement formatted as a string for display.
|
50
34
|
# @return [String]
|
51
35
|
def display_str
|
52
|
-
place_pub =
|
53
|
-
edition_place_pub =
|
54
|
-
|
36
|
+
place_pub = Utils.compact_and_join([place_str, publisher_str], delimiter: " : ")
|
37
|
+
edition_place_pub = Utils.compact_and_join([edition_str, place_pub], delimiter: " - ")
|
38
|
+
Utils.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
|
55
39
|
end
|
56
40
|
|
57
41
|
# Were any of the dates encoded?
|
@@ -65,25 +49,25 @@ module CocinaDisplay
|
|
65
49
|
# The date portion of the imprint statement, comprising all unique dates.
|
66
50
|
# @return [String]
|
67
51
|
def date_str
|
68
|
-
|
52
|
+
Utils.compact_and_join(unique_dates_for_display.map(&:qualified_value))
|
69
53
|
end
|
70
54
|
|
71
55
|
# The editions portion of the imprint statement, combining all edition notes.
|
72
56
|
# @return [String]
|
73
57
|
def edition_str
|
74
|
-
|
58
|
+
Utils.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
|
75
59
|
end
|
76
60
|
|
77
61
|
# The place of publication, combining all location values.
|
78
62
|
# @return [String]
|
79
63
|
def place_str
|
80
|
-
|
64
|
+
Utils.compact_and_join(locations_for_display, delimiter: " : ")
|
81
65
|
end
|
82
66
|
|
83
67
|
# The publisher information, combining all name values for publishers.
|
84
68
|
# @return [String]
|
85
69
|
def publisher_str
|
86
|
-
|
70
|
+
Utils.compact_and_join(Janeway.enum_for("$.contributor[?@.role[?@.value == 'publisher']].name[*].value", cocina), delimiter: " : ")
|
87
71
|
end
|
88
72
|
|
89
73
|
# Get the place name for a location, decoding from MARC if necessary.
|
@@ -46,6 +46,16 @@ module CocinaDisplay
|
|
46
46
|
[new(strategy: :all, add_punctuation: false).build(titles)].flatten - full_title(titles)
|
47
47
|
end
|
48
48
|
|
49
|
+
# Like the full title, but with any non-sorting characters and punctuation removed.
|
50
|
+
# @param titles [Array<Hash>] The titles to consider.
|
51
|
+
# @param catalog_links [Array<Hash>] The folio catalog links to check for digital serials part labels.
|
52
|
+
# @return [Array<String>] The sort title value(s) for Solr - array due to possible parallelValue
|
53
|
+
def self.sort_title(titles, catalog_links: [])
|
54
|
+
part_label = catalog_links.find { |link| link["catalog"] == "folio" }&.fetch("partLabel", nil)
|
55
|
+
[new(strategy: :first, add_punctuation: false, only_one_parallel_value: false, part_label: part_label, sortable: true).build(titles)]
|
56
|
+
.flatten.compact.map { |title| title.gsub(/[[:punct:]]*/, "").strip }
|
57
|
+
end
|
58
|
+
|
49
59
|
# @param strategy [Symbol] ":first" selects a single title value based on precedence of
|
50
60
|
# primary, untyped, first occurrence. ":all" returns an array containing all the values.
|
51
61
|
# @param add_punctuation [boolean] whether the title should be formatted with punctuation (think of a structured
|
@@ -54,11 +64,13 @@ module CocinaDisplay
|
|
54
64
|
# of primary, untyped, first occurrence. When false, return an array containing all the parallel values.
|
55
65
|
# Why? Think of e.g. title displayed in blacklight search results vs boosting values for ranking of search results
|
56
66
|
# @param part_label [String] the partLabel to add for digital serials display
|
57
|
-
|
67
|
+
# @param sortable [boolean] whether the title is intended for sorting, and should have non-sorting parts removed
|
68
|
+
def initialize(strategy:, add_punctuation:, only_one_parallel_value: true, part_label: nil, sortable: false)
|
58
69
|
@strategy = strategy
|
59
70
|
@add_punctuation = add_punctuation
|
60
71
|
@only_one_parallel_value = only_one_parallel_value
|
61
72
|
@part_label = part_label
|
73
|
+
@sortable = sortable
|
62
74
|
end
|
63
75
|
|
64
76
|
# @param [Array<Hash>] cocina_titles the titles to consider
|
@@ -161,6 +173,10 @@ module CocinaDisplay
|
|
161
173
|
@only_one_parallel_value
|
162
174
|
end
|
163
175
|
|
176
|
+
def sortable?
|
177
|
+
@sortable
|
178
|
+
end
|
179
|
+
|
164
180
|
# @return [Hash, nil] title that has status=primary
|
165
181
|
def primary_title(cocina_titles)
|
166
182
|
primary_title = cocina_titles.find { |title| title["status"] == "primary" }
|
@@ -221,7 +237,7 @@ module CocinaDisplay
|
|
221
237
|
# rubocop:disable Metrics/CyclomaticComplexity
|
222
238
|
# rubocop:disable Metrics/MethodLength
|
223
239
|
# rubocop:disable Metrics/PerceivedComplexity
|
224
|
-
def rebuild_structured_value(cocina_title)
|
240
|
+
def rebuild_structured_value(cocina_title, sortable: false)
|
225
241
|
result = ""
|
226
242
|
part_name_number = ""
|
227
243
|
cocina_title["structuredValue"].each do |structured_value| # rubocop:disable Metrics/BlockLength
|
@@ -235,8 +251,10 @@ module CocinaDisplay
|
|
235
251
|
# additional types ignored here, e.g. name, uniform ...
|
236
252
|
case structured_value["type"]&.downcase
|
237
253
|
when "nonsorting characters"
|
238
|
-
|
239
|
-
|
254
|
+
unless sortable?
|
255
|
+
padding = non_sorting_padding(cocina_title, value)
|
256
|
+
result = add_non_sorting_value(result, value, padding)
|
257
|
+
end
|
240
258
|
when "part name", "part number"
|
241
259
|
# even if there is a partLabel, use any existing structuredValue
|
242
260
|
# part name/number that remains for non-digital serials purposes
|
@@ -286,7 +304,7 @@ module CocinaDisplay
|
|
286
304
|
# rubocop:disable Metrics/PerceivedComplexity
|
287
305
|
# rubocop:disable Metrics/AbcSize
|
288
306
|
# rubocop:disable Metrics/CyclomaticComplexity
|
289
|
-
def main_title_from_structured_values(cocina_title)
|
307
|
+
def main_title_from_structured_values(cocina_title, sortable: false)
|
290
308
|
result = ""
|
291
309
|
# combine pieces of the cocina structuredValue into a single title
|
292
310
|
cocina_title["structuredValue"].each do |structured_value|
|
@@ -299,8 +317,10 @@ module CocinaDisplay
|
|
299
317
|
|
300
318
|
case structured_value["type"]&.downcase
|
301
319
|
when "nonsorting characters"
|
302
|
-
|
303
|
-
|
320
|
+
unless sortable?
|
321
|
+
padding = non_sorting_padding(cocina_title, value)
|
322
|
+
result = add_non_sorting_value(result, value, padding)
|
323
|
+
end
|
304
324
|
when "main title", "title"
|
305
325
|
result = if ["'", "-"].include?(result.last)
|
306
326
|
[result, value].join
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
# Helper methods for string formatting, etc.
|
3
|
+
module Utils
|
4
|
+
# Join non-empty values into a string using provided delimiter.
|
5
|
+
# If values already end in delimiter (ignoring whitespace), join with a space instead.
|
6
|
+
# @param values [Array<String>] The values to compact and join
|
7
|
+
# @param delimiter [String] The delimiter to use for joining, default is space
|
8
|
+
# @return [String] The compacted and joined string
|
9
|
+
def self.compact_and_join(values, delimiter: " ")
|
10
|
+
compacted_values = values.compact_blank.map(&:strip)
|
11
|
+
return compacted_values.first if compacted_values.one?
|
12
|
+
|
13
|
+
compacted_values.reduce(+"") do |result, value|
|
14
|
+
result << if value.end_with?(delimiter.strip)
|
15
|
+
value + " "
|
16
|
+
else
|
17
|
+
value + delimiter
|
18
|
+
end
|
19
|
+
end.delete_suffix(delimiter)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Recursively flatten structured values in Cocina metadata.
|
23
|
+
# Returns a list of hashes representing the "leaf" nodes with values.
|
24
|
+
# @return [Array<Hash>] List of node hashes with "value" present
|
25
|
+
def self.flatten_structured_values(cocina, output = [])
|
26
|
+
return [cocina] if cocina["value"].present?
|
27
|
+
return cocina.flat_map { |node| flatten_structured_values(node, output) } if cocina.is_a?(Array)
|
28
|
+
return output unless (structured_values = Array(cocina["structuredValue"])).present?
|
29
|
+
|
30
|
+
structured_values.flat_map { |node| flatten_structured_values(node, output) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
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: 0.
|
4
|
+
version: 0.4.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-07-
|
11
|
+
date: 2025-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: janeway-jsonpath
|
@@ -176,12 +176,17 @@ files:
|
|
176
176
|
- Rakefile
|
177
177
|
- lib/cocina_display.rb
|
178
178
|
- lib/cocina_display/cocina_record.rb
|
179
|
+
- lib/cocina_display/concerns/contributors.rb
|
179
180
|
- lib/cocina_display/concerns/events.rb
|
181
|
+
- lib/cocina_display/concerns/identifiers.rb
|
182
|
+
- lib/cocina_display/concerns/titles.rb
|
183
|
+
- lib/cocina_display/contributor.rb
|
180
184
|
- lib/cocina_display/dates/date.rb
|
181
185
|
- lib/cocina_display/dates/date_range.rb
|
182
186
|
- lib/cocina_display/imprint.rb
|
183
187
|
- lib/cocina_display/marc_country_codes.rb
|
184
188
|
- lib/cocina_display/title_builder.rb
|
189
|
+
- lib/cocina_display/utils.rb
|
185
190
|
- lib/cocina_display/version.rb
|
186
191
|
- sig/cocina_display.rbs
|
187
192
|
homepage: https://sul-dlss.github.io/cocina_display/
|