cocina_display 2.1.0 → 2.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.
@@ -1,142 +0,0 @@
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
- # The value to use for display.
20
- # @return [String, nil]
21
- def to_s
22
- flat_value
23
- end
24
-
25
- # Delimiter used to join multiple values for display.
26
- # @return [String, nil]
27
- def delimiter
28
- " -- " if table_of_contents?
29
- end
30
-
31
- # Does this note use a delimiter?
32
- # @return [Boolean]
33
- def delimited?
34
- delimiter.present?
35
- end
36
-
37
- # Single concatenated string value for the note.
38
- # @return [String, nil]
39
- def flat_value
40
- Utils.compact_and_join(values, delimiter: delimiter || "").presence
41
- end
42
-
43
- # The raw values from the Cocina data, flattened if nested.
44
- # Strips excess whitespace and the delimiter if present.
45
- # Splits on the delimiter if it was already included in the values(s).
46
- # @return [Array<String>]
47
- def values
48
- Utils.flatten_nested_values(cocina).pluck("value")
49
- .map { |value| cleaned_value(value) }
50
- .flat_map { |value| delimited? ? value.split(delimiter.strip) : [value] }
51
- .compact_blank
52
- end
53
-
54
- # The raw values from the Cocina data as a hash with type as key.
55
- # Strips excess whitespace and the delimiter if present.
56
- # Splits on the delimiter if it was already included in the values(s).
57
- # @return [Hash{String => Array<String>}]
58
- def values_by_type
59
- Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
60
- value = cleaned_value(node["value"])
61
- (delimited? ? value.split(delimiter.strip) : [value]).each do |part|
62
- type = node["type"]
63
- hash[type] ||= []
64
- hash[type] << part
65
- end
66
- end
67
- end
68
-
69
- # The type of the note, e.g. "abstract".
70
- # @return [String, nil]
71
- def type
72
- cocina["type"].presence
73
- end
74
-
75
- # The display label set in Cocina
76
- # @return [String, nil]
77
- def display_label
78
- cocina["displayLabel"].presence
79
- end
80
-
81
- # Label used to render the note for display.
82
- # Uses a displayLabel if available, otherwise tries to look up via type.
83
- # Falls back to a default label derived from the type or a generic note label if
84
- # no type is set.
85
- # @return [String]
86
- def label
87
- display_label ||
88
- I18n.t(type&.parameterize&.underscore, default: default_label, scope: "cocina_display.field_label.note")
89
- end
90
-
91
- # Check if the note is an abstract
92
- # @return [Boolean]
93
- def abstract?
94
- display_label&.match?(ABSTRACT_DISPLAY_LABEL_REGEX) ||
95
- ABSTRACT_TYPES.include?(type)
96
- end
97
-
98
- # Check if the note is a general note (not a table of contents, abstract, preferred citation, or part)
99
- # @return [Boolean]
100
- def general_note?
101
- !table_of_contents? && !abstract? && !preferred_citation? && !part?
102
- end
103
-
104
- # Check if the note is a preferred citation
105
- # @return [Boolean]
106
- def preferred_citation?
107
- display_label&.match?(PREFERRED_CITATION_DISPLAY_LABEL_REGEX) ||
108
- PREFERRED_CITATION_TYPES.include?(type)
109
- end
110
-
111
- # Check if the note is a table of contents
112
- # @return [Boolean]
113
- def table_of_contents?
114
- display_label&.match?(TOC_DISPLAY_LABEL_REGEX) ||
115
- TOC_TYPES.include?(type)
116
- end
117
-
118
- # Check if the note is a part note
119
- # @note These are combined with the title and not displayed separately.
120
- # @return [Boolean]
121
- def part?
122
- type == "part"
123
- end
124
-
125
- private
126
-
127
- def default_label
128
- type&.capitalize || I18n.t("cocina_display.field_label.note.note")
129
- end
130
-
131
- # Remove the delimiter from the ends of the value and strip whitespace.
132
- # @param value [String]
133
- # @return [String]
134
- def cleaned_value(value)
135
- return value.strip unless delimited?
136
-
137
- value.gsub(/\s*#{Regexp.escape(delimiter.strip)}\s*$/, " ")
138
- .gsub(/^\s*#{Regexp.escape(delimiter.strip)}\s*/, " ")
139
- .strip
140
- end
141
- end
142
- end
@@ -1,213 +0,0 @@
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, without trailing punctuation.
70
- # @note This corresponds to the entire MARC 245 field.
71
- # @return [String, nil]
72
- def display_title
73
- display_title_str.presence || cocina["value"]
74
- end
75
-
76
- # A string value for sorting by title.
77
- # Ignores punctuation, leading/trailing spaces, and non-sorting characters.
78
- # If no title is present, returns a high Unicode value so it sorts last.
79
- # @return [String]
80
- def sort_title
81
- return "\u{10FFFF}" unless full_title
82
-
83
- full_title[nonsorting_chars_str.length..]
84
- .unicode_normalize(:nfd) # Prevent accents being stripped
85
- .gsub(/[[:punct:]]*/, "")
86
- .gsub(/\W{2,}/, " ") # Collapse whitespace after removing punctuation
87
- .strip
88
- end
89
-
90
- private
91
-
92
- # Generate the short title by joining main title and nonsorting characters.
93
- # @return [String, nil]
94
- def short_title_str
95
- nonsorting_chars_str + main_title_str # pre-formatted padding
96
- end
97
-
98
- # Generate the full title by joining all title components with punctuation.
99
- # @return [String, nil]
100
- def full_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
107
- end
108
-
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/, "")
113
- end
114
-
115
- # The main title and subtitle, joined together with a colon.
116
- # @return [String, nil]
117
- def main_subtitle_str
118
- Utils.compact_and_join([main_title_str, subtitle_str], delimiter: " : ")
119
- end
120
-
121
- # All nonsorting characters joined together with padding applied.
122
- # Handles languages that do not separate nonsorting characters with spaces.
123
- # @return [String, nil]
124
- def nonsorting_chars_str
125
- pad_nonsorting(Utils.compact_and_join(Array(title_components["nonsorting characters"])))
126
- end
127
-
128
- # The main title component(s), joined together.
129
- # @return [String, nil]
130
- def main_title_str
131
- Utils.compact_and_join(Array(title_components["main title"]))
132
- end
133
-
134
- # The subtitle components, joined together.
135
- # @return [String, nil]
136
- def subtitle_str
137
- Utils.compact_and_join(Array(title_components["subtitle"]))
138
- end
139
-
140
- # The part name, number, and label components, joined together with commas.
141
- # @return [String, nil]
142
- def parts_str
143
- Utils.compact_and_join(
144
- Array(title_components["part number"] || @part_numbers) +
145
- Array(title_components["part name"]) +
146
- [@part_label],
147
- delimiter: ", "
148
- )
149
- end
150
-
151
- # The associated names, joined together with periods.
152
- # @note Only present for uniform titles.
153
- # @return [String, nil]
154
- def names_str
155
- Utils.compact_and_join(names, delimiter: ". ")
156
- end
157
-
158
- # Destructured title components, organized by type.
159
- # Unstructured titles and components with no type are grouped under "main title".
160
- # @return [Hash<String, Array<String>>]
161
- # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#title-part-types-for-structured-value
162
- def title_components
163
- Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
164
- type = case node["type"]
165
- when "uniform", "alternative", "abbreviated", "translated", "transliterated", "parallel", "supplied", nil
166
- "main title"
167
- else
168
- node["type"]
169
- end
170
- hash[type] ||= []
171
- hash[type] << node["value"]
172
- end.compact_blank
173
- end
174
-
175
- # Uniform titles can have associated person names.
176
- # @return [String, nil]
177
- def names
178
- Janeway.enum_for("$.note[?(@.type=='associated name')]", cocina).map do |name|
179
- Contributors::Name.new(name).to_s(with_date: true)
180
- end
181
- end
182
-
183
- # Number of nonsorting characters to ignore at the start of the title.
184
- # @return [Integer, nil]
185
- def nonsorting_char_count
186
- Janeway.enum_for("$.note[?(@.type=='nonsorting character count')].value", cocina).first&.to_i
187
- end
188
-
189
- # Type-specific label for the title, falling back to a generic "Title".
190
- # @return [String]
191
- def type_label
192
- I18n.t(type&.parameterize&.underscore, scope: "cocina_display.field_label.title", default: :title)
193
- end
194
-
195
- # Add or remove padding from nonsorting portion of the title.
196
- # @param value [String]
197
- # @return [String]
198
- def pad_nonsorting(value)
199
- case value.strip
200
- when /.*-$/, /.*'$/, "ה" # Arabic, French, Hebrew prefixes use no padding
201
- value.strip
202
- when "" # No nonsorting characters; return empty string
203
- ""
204
- else # Pad to nonsorting char count if set, otherwise add a single space
205
- if nonsorting_char_count.present?
206
- value.ljust(nonsorting_char_count, " ")
207
- else
208
- value + " "
209
- end
210
- end
211
- end
212
- end
213
- end