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.
- checksums.yaml +4 -4
- data/config/locales/en.yml +1 -0
- data/lib/cocina_display/concerns/geospatial.rb +8 -8
- data/lib/cocina_display/concerns/languages.rb +3 -3
- data/lib/cocina_display/concerns/notes.rb +1 -1
- data/lib/cocina_display/concerns/subjects.rb +28 -28
- data/lib/cocina_display/concerns/titles.rb +15 -14
- data/lib/cocina_display/concerns/url_helpers.rb +5 -2
- data/lib/cocina_display/contributors/contributor.rb +2 -12
- data/lib/cocina_display/contributors/name.rb +8 -93
- data/lib/cocina_display/contributors/name_value.rb +78 -0
- data/lib/cocina_display/languages/language.rb +91 -0
- data/lib/cocina_display/languages/script.rb +24 -0
- data/lib/cocina_display/notes/note.rb +37 -0
- data/lib/cocina_display/notes/note_value.rb +123 -0
- data/lib/cocina_display/parallel/parallel.rb +127 -0
- data/lib/cocina_display/parallel/parallel_value.rb +120 -0
- data/lib/cocina_display/related_resource.rb +11 -2
- data/lib/cocina_display/subjects/subject.rb +12 -57
- data/lib/cocina_display/subjects/subject_part.rb +177 -0
- data/lib/cocina_display/subjects/subject_value.rb +29 -160
- data/lib/cocina_display/titles/title.rb +49 -0
- data/lib/cocina_display/titles/title_value.rb +181 -0
- data/lib/cocina_display/version.rb +1 -1
- data/script/deep_compact.rb +2 -0
- data/script/find_records.rb +1 -1
- metadata +13 -6
- data/lib/cocina_display/language.rb +0 -53
- data/lib/cocina_display/note.rb +0 -142
- data/lib/cocina_display/title.rb +0 -213
data/lib/cocina_display/note.rb
DELETED
|
@@ -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
|
data/lib/cocina_display/title.rb
DELETED
|
@@ -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
|