cocina_display 1.1.3 → 1.2.1

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 (58) 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/config/marc_countries.yml +385 -0
  9. data/config/marc_relators.yml +310 -0
  10. data/config/searchworks_languages.yml +520 -0
  11. data/lib/cocina_display/cocina_record.rb +29 -64
  12. data/lib/cocina_display/concerns/accesses.rb +78 -0
  13. data/lib/cocina_display/concerns/contributors.rb +32 -11
  14. data/lib/cocina_display/concerns/events.rb +19 -6
  15. data/lib/cocina_display/concerns/forms.rb +98 -11
  16. data/lib/cocina_display/concerns/geospatial.rb +9 -5
  17. data/lib/cocina_display/concerns/identifiers.rb +25 -5
  18. data/lib/cocina_display/concerns/languages.rb +6 -2
  19. data/lib/cocina_display/concerns/notes.rb +36 -0
  20. data/lib/cocina_display/concerns/related_resources.rb +20 -0
  21. data/lib/cocina_display/concerns/subjects.rb +25 -8
  22. data/lib/cocina_display/concerns/titles.rb +67 -25
  23. data/lib/cocina_display/concerns/{access.rb → url_helpers.rb} +3 -3
  24. data/lib/cocina_display/concerns.rb +6 -0
  25. data/lib/cocina_display/contributors/contributor.rb +47 -26
  26. data/lib/cocina_display/contributors/name.rb +18 -14
  27. data/lib/cocina_display/contributors/role.rb +31 -13
  28. data/lib/cocina_display/dates/date.rb +55 -14
  29. data/lib/cocina_display/dates/date_range.rb +0 -2
  30. data/lib/cocina_display/description/access.rb +41 -0
  31. data/lib/cocina_display/description/access_contact.rb +11 -0
  32. data/lib/cocina_display/description/url.rb +17 -0
  33. data/lib/cocina_display/display_data.rb +104 -0
  34. data/lib/cocina_display/events/event.rb +8 -4
  35. data/lib/cocina_display/events/imprint.rb +0 -10
  36. data/lib/cocina_display/events/location.rb +9 -3
  37. data/lib/cocina_display/events/note.rb +33 -0
  38. data/lib/cocina_display/forms/form.rb +71 -0
  39. data/lib/cocina_display/forms/genre.rb +12 -0
  40. data/lib/cocina_display/forms/resource_type.rb +38 -0
  41. data/lib/cocina_display/geospatial.rb +1 -1
  42. data/lib/cocina_display/identifier.rb +101 -0
  43. data/lib/cocina_display/json_backed_record.rb +27 -0
  44. data/lib/cocina_display/language.rb +18 -12
  45. data/lib/cocina_display/license.rb +32 -0
  46. data/lib/cocina_display/note.rb +103 -0
  47. data/lib/cocina_display/related_resource.rb +74 -0
  48. data/lib/cocina_display/subjects/subject.rb +32 -9
  49. data/lib/cocina_display/subjects/subject_value.rb +34 -16
  50. data/lib/cocina_display/title.rb +221 -0
  51. data/lib/cocina_display/utils.rb +4 -4
  52. data/lib/cocina_display/version.rb +1 -1
  53. data/lib/cocina_display.rb +32 -2
  54. metadata +46 -12
  55. data/lib/cocina_display/title_builder.rb +0 -397
  56. data/lib/cocina_display/vocabularies/marc_country_codes.rb +0 -393
  57. data/lib/cocina_display/vocabularies/marc_relator_codes.rb +0 -318
  58. data/lib/cocina_display/vocabularies/searchworks_languages.rb +0 -526
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ # A data structure to be rendered into HTML by the consumer.
5
+ class DisplayData
6
+ class << self
7
+ # Given objects that support #to_s and #label, group them into {DisplayData}.
8
+ # Groups by each object's +label+ and keeps unique, non-blank values.
9
+ # @param objects [Array<Object>]
10
+ # @return [Array<DisplayData>]
11
+ def from_objects(objects)
12
+ objects.group_by(&:label)
13
+ .map { |label, objs| new(label: label, objects: objs) }
14
+ .reject { |data| data.values.empty? }
15
+ end
16
+
17
+ # Given an array of Cocina hashes, group them into {DisplayData}.
18
+ # Uses +label+ as the label if provided, but honors +displayLabel+ if set.
19
+ # Keeps the unique, non-blank values under each label.
20
+ # @param cocina [Array<Hash>]
21
+ # @param label [String]
22
+ # @return [Array<DisplayData>]
23
+ def from_cocina(cocina, label: nil)
24
+ from_objects(descriptive_values_from_cocina(cocina, label: label))
25
+ end
26
+
27
+ # Create display data from string values.
28
+ # @param value [String] The string values to display
29
+ # @param label [String] The label for the display data
30
+ # @return [Array<DisplayData>] The display data
31
+ def from_strings(values, label: nil)
32
+ from_objects(descriptive_values_from_strings(values, label: label))
33
+ end
34
+
35
+ # Create an array containing a descriptive object from string values.
36
+ # Can be used to combine a string derived value with other metadata objects.
37
+ # @param strings [Array<String>] The string values to display
38
+ # @param label [String] The label for the display data
39
+ # @return [Array<DescriptiveValue>] The descriptive values
40
+ def descriptive_values_from_strings(strings, label: nil)
41
+ strings.map { |string| DescriptiveValue.new(label: label, value: string) }
42
+ end
43
+
44
+ # Take one or several DisplayData and merge into a single hash.
45
+ # Keys are labels; values are the merged array of values for that label.
46
+ # @param display_data [DisplayData, Array<DisplayData>]
47
+ # @return [Hash{String => Array<String>}] The merged hash
48
+ def to_hash(display_data)
49
+ Array(display_data).map(&:to_h).reduce(:merge)
50
+ end
51
+
52
+ private
53
+
54
+ # Wrap Cocina nodes into {DescriptiveValue} so they are labelled.
55
+ # Uses +displayLabel+ from the node if present, otherwise uses the provided label.
56
+ # @param cocina [Array<Hash>]
57
+ # @param label [String]
58
+ # @return [Array<DescriptiveValue>]
59
+ def descriptive_values_from_cocina(cocina, label: nil)
60
+ cocina.map { |node| DescriptiveValue.new(label: node["displayLabel"] || label, value: node["value"]) }
61
+ end
62
+
63
+ # Wrapper to make Cocina descriptive values respond to #to_s and #label.
64
+ # @attr [String] label
65
+ # @attr [String] value
66
+ DescriptiveValue = Data.define(:label, :value) do
67
+ def to_s
68
+ value
69
+ end
70
+ end
71
+ end
72
+
73
+ # Create a DisplayData object from a list of objects that share a label
74
+ # @param label [String]
75
+ # @param objects [Array<#to_s>]
76
+ def initialize(label:, objects:)
77
+ @label = label
78
+ @objects = objects
79
+ end
80
+
81
+ attr_reader :label, :objects
82
+
83
+ # The unique, non-blank values for display
84
+ # @return [Array<String>]
85
+ def values
86
+ objects.flat_map { |object| split_string_on_newlines(object.to_s) }.compact_blank.uniq
87
+ end
88
+
89
+ # Express the display data as a hash mapping the label to its values.
90
+ # @return [Hash{String => Array<String>}] The label and values
91
+ def to_h
92
+ {label => values}
93
+ end
94
+
95
+ private
96
+
97
+ # Split a string on newlines (including HTML-encoded newlines) and strip whitespace.
98
+ # @param string [String] The string to split
99
+ # @return [Array<String>]
100
+ def split_string_on_newlines(string)
101
+ string&.gsub("&#10;", "\n")&.split("\n")&.map(&:strip)
102
+ end
103
+ end
104
+ end
@@ -1,7 +1,3 @@
1
- require_relative "location"
2
- require_relative "../dates/date"
3
- require_relative "../contributors/contributor"
4
-
5
1
  module CocinaDisplay
6
2
  module Events
7
3
  # An event associated with an object, like publication.
@@ -73,6 +69,14 @@ module CocinaDisplay
73
69
  CocinaDisplay::Events::Location.new(location)
74
70
  end
75
71
  end
72
+
73
+ # All notes associated with this event.
74
+ # @return [Array<CocinaDisplay::Events::Note>]
75
+ def notes
76
+ @notes ||= Array(cocina["note"]).map do |note|
77
+ CocinaDisplay::Events::Note.new(note)
78
+ end
79
+ end
76
80
  end
77
81
  end
78
82
  end
@@ -1,15 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "edtf"
4
- require "active_support"
5
- require "active_support/core_ext/enumerable"
6
- require "active_support/core_ext/object/blank"
7
-
8
- require_relative "event"
9
- require_relative "../utils"
10
- require_relative "../dates/date"
11
- require_relative "../dates/date_range"
12
-
13
3
  module CocinaDisplay
14
4
  module Events
15
5
  # Wrapper for Cocina events used to generate an imprint statement for display.
@@ -1,11 +1,17 @@
1
- require_relative "../vocabularies/marc_country_codes"
2
-
3
1
  module CocinaDisplay
4
2
  module Events
5
3
  # A single location represented in a Cocina event, like a publication place.
6
4
  class Location
5
+ MARC_COUNTRIES_FILE_PATH = CocinaDisplay.root / "config" / "marc_countries.yml"
6
+
7
7
  attr_reader :cocina
8
8
 
9
+ # A hash mapping MARC country codes to their names.
10
+ # @return [Hash{String => String}]
11
+ def self.marc_countries
12
+ @marc_countries ||= YAML.safe_load_file(MARC_COUNTRIES_FILE_PATH)
13
+ end
14
+
9
15
  # Initialize a Location object with Cocina structured data.
10
16
  # @param cocina [Hash] The Cocina structured data for the location.
11
17
  def initialize(cocina)
@@ -36,7 +42,7 @@ module CocinaDisplay
36
42
  # Decoded country name if the location is encoded with a MARC country code.
37
43
  # @return [String, nil]
38
44
  def decoded_country
39
- Vocabularies::MARC_COUNTRY[code] if marc_country? && valid_country_code?
45
+ Location.marc_countries[code] if marc_country? && valid_country_code?
40
46
  end
41
47
 
42
48
  # Is this a decodable country code?
@@ -0,0 +1,33 @@
1
+ module CocinaDisplay
2
+ module Events
3
+ # A single note represented in a Cocina event, like an issuance or edition note.
4
+ class Note
5
+ attr_reader :cocina
6
+
7
+ # Initialize a Note object with Cocina structured data.
8
+ # @param cocina [Hash] The Cocina structured data for the note.
9
+ def initialize(cocina)
10
+ @cocina = cocina
11
+ end
12
+
13
+ # The value of the note.
14
+ # @return [String, nil]
15
+ def to_s
16
+ cocina["value"].presence
17
+ end
18
+
19
+ # The type of the note, like "issuance" or "edition".
20
+ # @return [String, nil]
21
+ def type
22
+ cocina["type"].presence
23
+ end
24
+
25
+ # The display label for the note.
26
+ # @return [String]
27
+ def label
28
+ cocina["displayLabel"].presence ||
29
+ I18n.t(type&.parameterize&.underscore, default: :default, scope: "cocina_display.field_label.event.note")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ # Classes for extracting format/genre information from a Cocina object.
5
+ module Forms
6
+ # A form associated with part or all of a Cocina object.
7
+ class Form
8
+ attr_reader :cocina
9
+
10
+ # Create a Form object from Cocina structured data.
11
+ # Delegates to subclasses for specific types.
12
+ # @param cocina [Hash]
13
+ # @return [Form]
14
+ def self.from_cocina(cocina)
15
+ case cocina["type"]
16
+ when "genre"
17
+ Genre.new(cocina)
18
+ when "resource type"
19
+ ResourceType.new(cocina)
20
+ else
21
+ new(cocina)
22
+ end
23
+ end
24
+
25
+ # Create a Form object from Cocina structured data.
26
+ # @param cocina [Hash]
27
+ def initialize(cocina)
28
+ @cocina = cocina
29
+ end
30
+
31
+ # The value to use for display.
32
+ # @return [String]
33
+ def to_s
34
+ flat_value
35
+ end
36
+
37
+ # Single concatenated string value for the form.
38
+ # @return [String]
39
+ def flat_value
40
+ Utils.compact_and_join(values, delimiter: " > ")
41
+ end
42
+
43
+ # The raw values from the Cocina data, flattened if nested.
44
+ # @return [String]
45
+ def values
46
+ Utils.flatten_nested_values(cocina).pluck("value").compact_blank
47
+ end
48
+
49
+ # The label to use for display.
50
+ # Uses a displayLabel if available, otherwise looks up via type.
51
+ # @return [String]
52
+ def label
53
+ cocina["displayLabel"].presence || type_label
54
+ end
55
+
56
+ # The type of form, such as "genre", "extent", etc.
57
+ # @return [String, nil]
58
+ def type
59
+ cocina["type"]
60
+ end
61
+
62
+ private
63
+
64
+ # Type-specific label for this form value.
65
+ # @return [String]
66
+ def type_label
67
+ I18n.t(type&.parameterize&.underscore, default: :form, scope: "cocina_display.field_label.form")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,12 @@
1
+ module CocinaDisplay
2
+ module Forms
3
+ # A Genre form associated with part or all of a Cocina object.
4
+ class Genre < Form
5
+ # Genres are capitalized for display.
6
+ # @return [String]
7
+ def to_s
8
+ super&.upcase_first
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module CocinaDisplay
2
+ module Forms
3
+ # A Resource Type form associated with part or all of a Cocina object.
4
+ class ResourceType < Form
5
+ # Resource types are lowercased for display.
6
+ # @return [String]
7
+ def to_s
8
+ super&.downcase
9
+ end
10
+
11
+ # Is this a Stanford self-deposit resource type?
12
+ # @note These are handled separately when displayed.
13
+ # @return [Boolean]
14
+ def stanford_self_deposit?
15
+ source == "Stanford self-deposit resource types"
16
+ end
17
+
18
+ # Is this a MODS resource type?
19
+ # @return [Boolean]
20
+ def mods?
21
+ source == "MODS resource types"
22
+ end
23
+
24
+ private
25
+
26
+ # @return [String]
27
+ def source
28
+ cocina.dig("source", "value")
29
+ end
30
+
31
+ # Stanford self-deposit resource types are labeled "Genre".
32
+ # @return [String]
33
+ def type_label
34
+ (I18n.t("cocina_display.field_label.form.genre") if stanford_self_deposit?) || super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,4 +1,4 @@
1
- require "geo/coord"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module CocinaDisplay
4
4
  module Geospatial
@@ -0,0 +1,101 @@
1
+ module CocinaDisplay
2
+ # An identifier for an object or a descriptive value.
3
+ class Identifier
4
+ attr_reader :cocina
5
+
6
+ # Source URI values for common identifiers
7
+ # If you have the bare ID, you can always add it to these to make a valid URL
8
+ SOURCE_URIS = {
9
+ "ORCID" => "https://orcid.org/",
10
+ "ROR" => "https://ror.org/",
11
+ "DOI" => "https://doi.org/",
12
+ "ISNI" => "https://isni.org/"
13
+ }.freeze
14
+
15
+ # Initialize an Identifier from Cocina structured data.
16
+ # @param cocina [Hash]
17
+ def initialize(cocina)
18
+ @cocina = cocina
19
+ end
20
+
21
+ # String representation of the identifier.
22
+ # Prefers the URI representation where present.
23
+ # @return [String]
24
+ def to_s
25
+ uri || value
26
+ end
27
+
28
+ def ==(other)
29
+ other.is_a?(Identifier) && other.cocina == cocina
30
+ end
31
+
32
+ # The raw value from the Cocina structured data.
33
+ # Prefers the URI representation where present.
34
+ # @return [String, nil]
35
+ def value
36
+ cocina["uri"].presence || cocina["value"].presence
37
+ end
38
+
39
+ # The "identifying" part of the identifier.
40
+ # Tries to parse from the end of the URI.
41
+ # @example DOI
42
+ # 10.1234/doi
43
+ # @return [String, nil]
44
+ def identifier
45
+ URI(value).path.delete_prefix("/") if value
46
+ end
47
+
48
+ # The identifier as a URI, if available.
49
+ # Tries to construct a URI if the parts are available to do so.
50
+ # @example DOI
51
+ # https://doi.org/10.1234/doi
52
+ # @return [String, nil]
53
+ def uri
54
+ cocina["uri"].presence || ([scheme_uri.delete_suffix("/"), identifier].join("/") if scheme_uri && identifier)
55
+ end
56
+
57
+ # The type of the identifier, e.g. "DOI".
58
+ # @return [String, nil]
59
+ def type
60
+ ("DOI" if doi?) || cocina["type"].presence
61
+ end
62
+
63
+ # The declared encoding of the identifier, if any.
64
+ # @return [String, nil]
65
+ def code
66
+ cocina.dig("source", "code").presence
67
+ end
68
+
69
+ # The base URI used to resolve the identifier, if any.
70
+ # @example DOI
71
+ # https://doi.org/
72
+ # @return [String, nil]
73
+ def scheme_uri
74
+ cocina.dig("source", "uri") || SOURCE_URIS[type]
75
+ end
76
+
77
+ # Label used to render the identifier for display.
78
+ # Uses a displayLabel if available, otherwise tries to look up via type.
79
+ # Falls back to a generic label for any unknown identifier types.
80
+ # @return [String]
81
+ def label
82
+ cocina["displayLabel"].presence ||
83
+ I18n.t(label_key, default: :identifier, scope: "cocina_display.field_label.identifier")
84
+ end
85
+
86
+ # Check if the identifier is a DOI.
87
+ # There are several indicators that could suggest this.
88
+ # @return [Boolean]
89
+ def doi?
90
+ cocina["type"]&.match?(/doi/i) || code == "doi" || cocina["uri"]&.include?("://doi.org")
91
+ end
92
+
93
+ private
94
+
95
+ # Key used for i18n lookup of the label, based on the type.
96
+ # @return [String, nil]
97
+ def label_key
98
+ type&.parameterize&.underscore
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ class JsonBackedRecord
5
+ # The parsed Cocina document.
6
+ # @return [Hash]
7
+ attr_reader :cocina_doc
8
+
9
+ # Initialize a CocinaRecord with a Cocina document hash.
10
+ # @param cocina_doc [Hash]
11
+ def initialize(cocina_doc)
12
+ @cocina_doc = cocina_doc
13
+ end
14
+
15
+ # Evaluate a JSONPath expression against the Cocina document.
16
+ # @return [Enumerator] An enumerator that yields results matching the expression.
17
+ # @param path_expression [String] The JSONPath expression to evaluate.
18
+ # @see https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md
19
+ # @example Name values for contributors
20
+ # record.path("$.description.contributor.*.name.*.value").search #=> ["Smith, John", "ACME Corp."]
21
+ # @example Filtering nodes using a condition
22
+ # record.path("$.description.contributor[?(@.type == 'person')].name.*.value").search #=> ["Smith, John"]
23
+ def path(path_expression)
24
+ Janeway.enum_for(path_expression, cocina_doc)
25
+ end
26
+ end
27
+ end
@@ -1,11 +1,18 @@
1
- require "iso639"
2
- require_relative "vocabularies/searchworks_languages"
1
+ # frozen_string_literal: true
3
2
 
4
3
  module CocinaDisplay
5
4
  # A language associated with part or all of a Cocina object.
6
5
  class Language
6
+ SEARCHWORKS_LANGUAGES_FILE_PATH = CocinaDisplay.root / "config" / "searchworks_languages.yml"
7
+
7
8
  attr_reader :cocina
8
9
 
10
+ # A hash of language codes to language names recognized by Searchworks.
11
+ # @return [Hash{String => String}]
12
+ def self.searchworks_languages
13
+ @searchworks_languages ||= YAML.safe_load_file(SEARCHWORKS_LANGUAGES_FILE_PATH)
14
+ end
15
+
9
16
  # Create a Language object from Cocina structured data.
10
17
  # @param cocina [Hash]
11
18
  def initialize(cocina)
@@ -27,21 +34,20 @@ module CocinaDisplay
27
34
  # Decoded name of the language based on the code, if present.
28
35
  # @return [String, nil]
29
36
  def decoded_value
30
- Vocabularies::SEARCHWORKS_LANGUAGES[code] || (Iso639[code] if iso_639?)
37
+ Language.searchworks_languages[code] if searchworks_language?
31
38
  end
32
39
 
33
- # True if the language is recognized by Searchworks.
34
- # @see CocinaDisplay::Vocabularies::SEARCHWORKS_LANGUAGES
35
- # @return [Boolean]
36
- def searchworks_language?
37
- Vocabularies::SEARCHWORKS_LANGUAGES.value?(to_s)
40
+ # Display label for this field.
41
+ # @return [String]
42
+ def label
43
+ cocina["displayLabel"].presence || I18n.t("cocina_display.field_label.language")
38
44
  end
39
45
 
40
- # True if the language has a code sourced from the ISO 639 vocabulary.
41
- # @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
46
+ # True if the language is recognized by Searchworks.
47
+ # @see CocinaDisplay::Language.searchworks_languages
42
48
  # @return [Boolean]
43
- def iso_639?
44
- cocina.dig("source", "code")&.start_with? "iso639"
49
+ def searchworks_language?
50
+ Language.searchworks_languages.value?(cocina["value"]) || Language.searchworks_languages.key?(code)
45
51
  end
46
52
  end
47
53
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ # A license associated with part or all of a Cocina object.
5
+ # This is the license entity used for translating a license URL into text
6
+ # for display.
7
+ class License
8
+ LICENSE_FILE_PATH = CocinaDisplay.root / "config" / "licenses.yml"
9
+
10
+ attr_reader :description, :uri
11
+
12
+ # Raised when the license provided is not valid
13
+ class LegacyLicenseError < StandardError; end
14
+
15
+ # A hash of license URLs to their description attributes
16
+ # @return [Hash{String => Hash{String => String}}]
17
+ def self.licenses
18
+ @licenses ||= YAML.safe_load_file(LICENSE_FILE_PATH)
19
+ end
20
+
21
+ # Initialize a License from a license URL.
22
+ # @param url [String] The license URL.
23
+ # @raise [LegacyLicenseError] if the license URL is not in the config
24
+ def initialize(url:)
25
+ raise LegacyLicenseError unless License.licenses.key?(url)
26
+
27
+ attrs = License.licenses.fetch(url)
28
+ @uri = url
29
+ @description = attrs.fetch("description")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,103 @@
1
+ module CocinaDisplay
2
+ # A note associated with a cocina record
3
+ class Note
4
+ ABSTRACT_TYPES = ["summary", "abstract", "scope and content"].freeze
5
+ ABSTRACT_DISPLAY_LABEL_REGEX = /Abstract|Summary|Scope and content/i
6
+ PREFERRED_CITATION_TYPES = ["preferred citation"].freeze
7
+ PREFERRED_CITATION_DISPLAY_LABEL_REGEX = /Preferred citation/i
8
+ TOC_TYPES = ["table of contents"].freeze
9
+ TOC_DISPLAY_LABEL_REGEX = /Table of contents/i
10
+
11
+ attr_reader :cocina
12
+
13
+ # Initialize a Note from Cocina structured data.
14
+ # @param cocina [Hash]
15
+ def initialize(cocina)
16
+ @cocina = cocina
17
+ end
18
+
19
+ # String representation of the note.
20
+ # @return [String, nil]
21
+ def to_s
22
+ Utils.compact_and_join(values, delimiter: " -- ").presence
23
+ end
24
+
25
+ # The raw values from the Cocina data, flattened if nested.
26
+ # @return [String]
27
+ def values
28
+ Utils.flatten_nested_values(cocina).pluck("value").compact_blank
29
+ end
30
+
31
+ # The raw values from the Cocina data as a hash with type as key.
32
+ # @return [Hash{String => String}]
33
+ def values_by_type
34
+ Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
35
+ type = node["type"]
36
+ hash[type] ||= []
37
+ hash[type] << node["value"]
38
+ end
39
+ end
40
+
41
+ # The type of the note, e.g. "abstract".
42
+ # @return [String, nil]
43
+ def type
44
+ cocina["type"].presence
45
+ end
46
+
47
+ # The display label set in Cocina
48
+ # @return [String, nil]
49
+ def display_label
50
+ cocina["displayLabel"].presence
51
+ end
52
+
53
+ # Label used to render the note for display.
54
+ # Uses a displayLabel if available, otherwise tries to look up via type.
55
+ # Falls back to a default label derived from the type or a generic note label if
56
+ # no type is set.
57
+ # @return [String]
58
+ def label
59
+ display_label ||
60
+ I18n.t(type&.parameterize&.underscore, default: default_label, scope: "cocina_display.field_label.note")
61
+ end
62
+
63
+ # Check if the note is an abstract
64
+ # @return [Boolean]
65
+ def abstract?
66
+ display_label&.match?(ABSTRACT_DISPLAY_LABEL_REGEX) ||
67
+ ABSTRACT_TYPES.include?(type)
68
+ end
69
+
70
+ # Check if the note is a general note (not a table of contents, abstract, preferred citation, or part)
71
+ # @return [Boolean]
72
+ def general_note?
73
+ !table_of_contents? && !abstract? && !preferred_citation? && !part?
74
+ end
75
+
76
+ # Check if the note is a preferred citation
77
+ # @return [Boolean]
78
+ def preferred_citation?
79
+ display_label&.match?(PREFERRED_CITATION_DISPLAY_LABEL_REGEX) ||
80
+ PREFERRED_CITATION_TYPES.include?(type)
81
+ end
82
+
83
+ # Check if the note is a table of contents
84
+ # @return [Boolean]
85
+ def table_of_contents?
86
+ display_label&.match?(TOC_DISPLAY_LABEL_REGEX) ||
87
+ TOC_TYPES.include?(type)
88
+ end
89
+
90
+ # Check if the note is a part note
91
+ # @note These are combined with the title and not displayed separately.
92
+ # @return [Boolean]
93
+ def part?
94
+ type == "part"
95
+ end
96
+
97
+ private
98
+
99
+ def default_label
100
+ type&.capitalize || I18n.t("cocina_display.field_label.note.note")
101
+ end
102
+ end
103
+ end