cocina_display 1.7.0 → 1.8.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/lib/cocina_display/concerns/contributors.rb +12 -4
- data/lib/cocina_display/concerns/events.rb +1 -2
- data/lib/cocina_display/concerns/forms.rb +4 -5
- data/lib/cocina_display/concerns/identifiers.rb +12 -5
- data/lib/cocina_display/concerns/notes.rb +19 -0
- data/lib/cocina_display/concerns/structural.rb +65 -7
- data/lib/cocina_display/dates/date.rb +26 -27
- data/lib/cocina_display/dates/date_range.rb +29 -1
- data/lib/cocina_display/forms/resource_type.rb +35 -2
- data/lib/cocina_display/geospatial.rb +6 -2
- data/lib/cocina_display/json_backed_record.rb +4 -1
- data/lib/cocina_display/structural/file.rb +134 -0
- data/lib/cocina_display/structural/file_set.rb +57 -0
- data/lib/cocina_display/title.rb +1 -1
- data/lib/cocina_display/utils.rb +2 -2
- data/lib/cocina_display/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b326b540fee8c0ad46a66aa4db50c2857328b08c42a57b0777b3f2e4ccc2a684
|
|
4
|
+
data.tar.gz: e8c17b5bcee9af19b32c98a29d60424ee693131f703936dbe10c91fd9a67caa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aea3773bf3e3aa17c1e150a33da7c906f178a1814cc8c911fff1dd2cc5d08c5969970d9d68ecb07c72e29b4692654935f66b44f4445ea239ec437c5ab1e98017
|
|
7
|
+
data.tar.gz: 409b98e5c82a5456cc137e07f5786ab2d5118ab267b5667938e0e1be9896a116b416a2bc73f077d61c4c434f83f1dc3a4f59abf05eeeb1ea3ec6195efd45aa09
|
|
@@ -108,11 +108,13 @@ module CocinaDisplay
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
# All contributors for the object, including authors, editors, etc.
|
|
111
|
-
#
|
|
111
|
+
# Checks both description.contributor and description.event.contributor.
|
|
112
112
|
# @return [Array<Contributor>]
|
|
113
113
|
def contributors
|
|
114
|
-
@contributors ||=
|
|
115
|
-
.
|
|
114
|
+
@contributors ||= Enumerator::Chain.new(
|
|
115
|
+
path("$.description.contributor.*"),
|
|
116
|
+
path("$.description.event.*.contributor.*")
|
|
117
|
+
).map { |c| CocinaDisplay::Contributors::Contributor.new(c) }
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
# All contributors with an "author" role.
|
|
@@ -145,7 +147,13 @@ module CocinaDisplay
|
|
|
145
147
|
# @return [Array<Contributor>]
|
|
146
148
|
def additional_contributors
|
|
147
149
|
return [] if contributors.empty? || contributors.one?
|
|
148
|
-
contributors - [main_contributor]
|
|
150
|
+
contributors.reject { |c| imprint_contributors.include?(c) } - [main_contributor]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The contributors associated with imprint events (usually publishers).
|
|
154
|
+
# @return [Array<Contributor>]
|
|
155
|
+
def imprint_contributors
|
|
156
|
+
imprint_events.flat_map(&:contributors).uniq
|
|
149
157
|
end
|
|
150
158
|
end
|
|
151
159
|
end
|
|
@@ -38,11 +38,10 @@ module CocinaDisplay
|
|
|
38
38
|
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
|
39
39
|
# @return [Array<Integer>, nil]
|
|
40
40
|
# @note 6 BCE will appear as -5; 4 CE will appear as 4.
|
|
41
|
-
def
|
|
41
|
+
def pub_year_ints(ignore_qualified: false)
|
|
42
42
|
date = pub_date(ignore_qualified: ignore_qualified)
|
|
43
43
|
return unless date
|
|
44
44
|
|
|
45
|
-
date = date.as_interval if date.is_a? CocinaDisplay::Dates::DateRange
|
|
46
45
|
date.to_a.map(&:year).compact.uniq.sort
|
|
47
46
|
end
|
|
48
47
|
|
|
@@ -199,15 +199,13 @@ module CocinaDisplay
|
|
|
199
199
|
when "manuscript", "mixed material"
|
|
200
200
|
values << "Archive/Manuscript"
|
|
201
201
|
when "moving image"
|
|
202
|
-
values << "Video"
|
|
202
|
+
values << "Video/Film"
|
|
203
203
|
when "notated music"
|
|
204
204
|
values << "Music score"
|
|
205
205
|
when "software, multimedia"
|
|
206
206
|
# Prevent GIS datasets from being labeled as "Software"
|
|
207
207
|
values << "Software/Multimedia" unless cartographic? || dataset?
|
|
208
|
-
when "sound recording-musical"
|
|
209
|
-
values << "Music recording"
|
|
210
|
-
when "sound recording-nonmusical", "sound recording"
|
|
208
|
+
when "sound recording-musical", "sound recording-nonmusical", "sound recording"
|
|
211
209
|
values << "Sound recording"
|
|
212
210
|
when "still image"
|
|
213
211
|
values << "Image"
|
|
@@ -216,7 +214,8 @@ module CocinaDisplay
|
|
|
216
214
|
# 2 records currently (2025) in Searchworks do this, but it is real.
|
|
217
215
|
if periodical? || archived_website?
|
|
218
216
|
values << "Journal/Periodical" if periodical?
|
|
219
|
-
values << "
|
|
217
|
+
values << "Website" if archived_website?
|
|
218
|
+
values << "Website|Archived website" if archived_website?
|
|
220
219
|
else
|
|
221
220
|
values << "Book"
|
|
222
221
|
end
|
|
@@ -8,8 +8,7 @@ module CocinaDisplay
|
|
|
8
8
|
# @example
|
|
9
9
|
# record.druid #=> "druid:bb099mt5053"
|
|
10
10
|
def druid
|
|
11
|
-
cocina_doc["externalIdentifier"] ||
|
|
12
|
-
cocina_doc.dig("description", "purl")&.split("/")&.last
|
|
11
|
+
cocina_doc["externalIdentifier"] || purl_url&.split("/")&.last
|
|
13
12
|
end
|
|
14
13
|
|
|
15
14
|
# The DRUID for the object, without the +druid:+ prefix.
|
|
@@ -64,10 +63,12 @@ module CocinaDisplay
|
|
|
64
63
|
folio_hrid || bare_druid
|
|
65
64
|
end
|
|
66
65
|
|
|
67
|
-
#
|
|
66
|
+
# All identifier objects, optionally filtered by type.
|
|
67
|
+
# @param type [String, nil] The type of identifier to filter by (e.g. "DOI").
|
|
68
|
+
# @note Type matching is case insensitive.
|
|
68
69
|
# @return [Array<Identifier>]
|
|
69
|
-
def identifiers
|
|
70
|
-
|
|
70
|
+
def identifiers(type: nil)
|
|
71
|
+
type.present? ? all_identifiers.filter { |id| id.type&.casecmp?(type) } : all_identifiers
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
# Labelled display data for identifiers.
|
|
@@ -78,6 +79,12 @@ module CocinaDisplay
|
|
|
78
79
|
|
|
79
80
|
private
|
|
80
81
|
|
|
82
|
+
# All identifier objects extracted from the Cocina metadata.
|
|
83
|
+
# @return [Array<Identifier>]
|
|
84
|
+
def all_identifiers
|
|
85
|
+
@identifiers ||= path("$.description.identifier[*]").map { |id| Identifier.new(id) } + Array(doi_from_identification)
|
|
86
|
+
end
|
|
87
|
+
|
|
81
88
|
# Synthetic Identifier object for a DOI in the identification block.
|
|
82
89
|
# @return [Array<Identifier>]
|
|
83
90
|
def doi_from_identification
|
|
@@ -8,6 +8,25 @@ module CocinaDisplay
|
|
|
8
8
|
@notes ||= path("$.description.note.*").map { |note| CocinaDisplay::Note.new(note) }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Text of all abstract notes.
|
|
12
|
+
# @return [Array<String>]
|
|
13
|
+
def abstracts
|
|
14
|
+
notes.select(&:abstract?).map(&:to_s).compact_blank
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Text of all table of contents notes.
|
|
18
|
+
# @return [Array<String>]
|
|
19
|
+
def tables_of_contents
|
|
20
|
+
notes.select(&:table_of_contents?).map(&:to_s).compact_blank
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Preferred citation for the object.
|
|
24
|
+
# If there are multiple notes with this type, uses the first.
|
|
25
|
+
# @return [String, nil]
|
|
26
|
+
def preferred_citation
|
|
27
|
+
notes.find(&:preferred_citation?)&.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
11
30
|
# Abstract metadata for display.
|
|
12
31
|
# @return [Array<CocinaDisplay::DisplayData>]
|
|
13
32
|
def abstract_display_data
|
|
@@ -4,23 +4,44 @@ module CocinaDisplay
|
|
|
4
4
|
module Concerns
|
|
5
5
|
# Methods for inspecting structural metadata (e.g. file hierarchy)
|
|
6
6
|
module Structural
|
|
7
|
+
# Structured data for all file sets in the object.
|
|
8
|
+
# Each fileset contains one or more files.
|
|
9
|
+
# @return [Array<CocinaDisplay::Structural::FileSet>]
|
|
10
|
+
# @example
|
|
11
|
+
# record.filesets.each do |fileset|
|
|
12
|
+
# puts fileset.type #=> "image"
|
|
13
|
+
# puts fileset.label #=> "High Resolution Images"
|
|
14
|
+
# end
|
|
15
|
+
def filesets
|
|
16
|
+
@filesets ||= path("$.structural.contains.*").map do |fileset|
|
|
17
|
+
CocinaDisplay::Structural::FileSet.new(fileset, druid: bare_druid, base_url: stacks_base_url)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
# Structured data for all individual files in the object.
|
|
8
22
|
# Traverses nested FileSet structure to return a flattened array.
|
|
9
|
-
# @return [Array<
|
|
23
|
+
# @return [Array<CocinaDisplay::Structural::File>]
|
|
10
24
|
# @example
|
|
11
25
|
# record.files.each do |file|
|
|
12
|
-
# puts file
|
|
13
|
-
# puts file
|
|
26
|
+
# puts file.filename #=> "image1.jpg"
|
|
27
|
+
# puts file.size #=> 123456
|
|
14
28
|
# end
|
|
15
29
|
def files
|
|
16
|
-
|
|
30
|
+
filesets.flat_map(&:files)
|
|
17
31
|
end
|
|
18
32
|
|
|
19
33
|
# All unique MIME types of files in this object.
|
|
20
34
|
# @return [Array<String>]
|
|
21
35
|
# @example ["image/jpeg", "application/pdf"]
|
|
22
36
|
def file_mime_types
|
|
23
|
-
files.
|
|
37
|
+
files.map(&:mime_type).compact.uniq
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# All unique types of filesets in this object.
|
|
41
|
+
# @return [Array<String>]
|
|
42
|
+
# @example ["image", "document"]
|
|
43
|
+
def fileset_types
|
|
44
|
+
filesets.map(&:type).compact.uniq
|
|
24
45
|
end
|
|
25
46
|
|
|
26
47
|
# Human-readable string representation of {total_file_size_int}.
|
|
@@ -34,7 +55,25 @@ module CocinaDisplay
|
|
|
34
55
|
# @return [Integer]
|
|
35
56
|
# @example 2621440
|
|
36
57
|
def total_file_size_int
|
|
37
|
-
files.
|
|
58
|
+
files.map(&:size).compact.sum
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# URL to a thumbnail image for this object, if any.
|
|
62
|
+
# @note Uses the IIIF image server to generate an image of the given size.
|
|
63
|
+
# @param region [String] Desired region of the image (e.g., "full", "square", "x,y,w,h", "pct:x,y,w,h").
|
|
64
|
+
# @param width [String] Desired width of the image in pixels (use "!" prefix to preserve aspect ratio).
|
|
65
|
+
# @param height [String] Desired height of the image in pixels.
|
|
66
|
+
# @return [String, nil]
|
|
67
|
+
# @example "https://stacks.stanford.edu/image/iiif/ts786ny5936%2FPC0170_s1_E_0204.jp2/full/!400,400/0/default.jpg"
|
|
68
|
+
def thumbnail_url(region: "full", width: "!400", height: "400")
|
|
69
|
+
thumbnail_file&.iiif_url(region: region, width: width, height: height)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# True if the object has a usable thumbnail file.
|
|
73
|
+
# @note Does not attempt to crawl virtual object members for thumbnails.
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
def thumbnail?
|
|
76
|
+
thumbnail_file.present?
|
|
38
77
|
end
|
|
39
78
|
|
|
40
79
|
# DRUIDs of collections this object is a member of.
|
|
@@ -44,17 +83,36 @@ module CocinaDisplay
|
|
|
44
83
|
path("$.structural.isMemberOf.*").map { |druid| druid.delete_prefix("druid:") }
|
|
45
84
|
end
|
|
46
85
|
|
|
86
|
+
# Whether this object is a virtual object.
|
|
87
|
+
# @return [Boolean]
|
|
47
88
|
def virtual_object?
|
|
48
|
-
return false if
|
|
89
|
+
return false if filesets.any?
|
|
49
90
|
|
|
50
91
|
path("$.structural.hasMemberOrders.*.members.*").any?
|
|
51
92
|
end
|
|
52
93
|
|
|
94
|
+
# DRUIDs of members of this virtual object.
|
|
95
|
+
# @return [Array<String>]
|
|
96
|
+
# @example ["ts786ny5936", "tp006ms8736", "tj297ys4758"]
|
|
53
97
|
def virtual_object_members
|
|
54
98
|
return [] unless virtual_object?
|
|
55
99
|
|
|
56
100
|
path("$.structural.hasMemberOrders.*.members.*").map { |druid| druid.delete_prefix("druid:") }
|
|
57
101
|
end
|
|
102
|
+
|
|
103
|
+
# DRUIDs of virtual objects this object is a part of.
|
|
104
|
+
# @return [Array<String>]
|
|
105
|
+
# @example "hj097bm8879"
|
|
106
|
+
def virtual_object_parents
|
|
107
|
+
related_resources.filter { |res| res.type == "part of" }.map(&:druid).compact_blank
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The thumbnail file for this object, if any.
|
|
111
|
+
# Prefers files marked as thumbnails; falls back to any JP2 image.
|
|
112
|
+
# @return [CocinaDisplay::Structural::File, nil]
|
|
113
|
+
def thumbnail_file
|
|
114
|
+
files.find(&:thumbnail?) || files.find(&:jp2_image?)
|
|
115
|
+
end
|
|
58
116
|
end
|
|
59
117
|
end
|
|
60
118
|
end
|
|
@@ -84,7 +84,7 @@ module CocinaDisplay
|
|
|
84
84
|
return
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
sanitized = value.gsub(
|
|
87
|
+
sanitized = value.gsub(/^\[+/, "").gsub(/[.\]]+$/, "")
|
|
88
88
|
sanitized = value.rjust(4, "0") if /^\d{3}$/.match?(value)
|
|
89
89
|
|
|
90
90
|
sanitized
|
|
@@ -291,25 +291,17 @@ module CocinaDisplay
|
|
|
291
291
|
return value.sub(/(\d{2})(\d{2})-(\d{2})/, '\1\2-\1\3')
|
|
292
292
|
end
|
|
293
293
|
|
|
294
|
-
value.gsub(/(
|
|
294
|
+
value.gsub(/(?<!\d)(\d{1,3})([xu-]{1,3})/i) { "#{Regexp.last_match(1)}#{"0" * Regexp.last_match(2).length}" }.scan(/[\d-]/).join
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
# Decoded version of the date with "BCE" or "CE". Strips leading zeroes.
|
|
298
298
|
# @param allowed_precisions [Array<Symbol>] List of allowed precisions for the output.
|
|
299
299
|
# Defaults to [:day, :month, :year, :decade, :century, :unknown].
|
|
300
|
-
# @param ignore_unparseable [Boolean] Return nil instead of the original value if it couldn't be parsed
|
|
301
|
-
# @param display_original_value [Boolean] Return the original value if it was not encoded
|
|
302
300
|
# @return [String]
|
|
303
|
-
def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown]
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if display_original_value
|
|
308
|
-
unless encoding?
|
|
309
|
-
return value.strip unless value =~ /^-?\d+$/ || value =~ /^[\dXxu?-]{4}$/
|
|
310
|
-
end
|
|
301
|
+
def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown])
|
|
302
|
+
if !parsed_date? || (!encoding? && value !~ /^-?\d+$/ && value !~ /^[\dXxu?-]{4}$/)
|
|
303
|
+
return value.strip
|
|
311
304
|
end
|
|
312
|
-
|
|
313
305
|
if date.is_a?(EDTF::Interval)
|
|
314
306
|
range = [
|
|
315
307
|
Date.format_date(date.min, date.min.precision, allowed_precisions),
|
|
@@ -342,18 +334,25 @@ module CocinaDisplay
|
|
|
342
334
|
format(qualified_format, decoded_value)
|
|
343
335
|
end
|
|
344
336
|
|
|
345
|
-
# Range between earliest possible date and latest possible date.
|
|
346
|
-
# @note
|
|
347
|
-
# @
|
|
337
|
+
# Range of {Date}s between earliest possible date and latest possible date.
|
|
338
|
+
# @note Output has day precision, using the first day/month if unspecified.
|
|
339
|
+
# @note If the range is open-ended, uses today's date as the end date.
|
|
340
|
+
# @note {EDTF::Set}s can be disjoint ranges, but this method will return the full span, unlike {#to_a}.
|
|
341
|
+
# @return [Range<Date>, nil]
|
|
348
342
|
def as_range
|
|
349
|
-
return unless earliest_date
|
|
343
|
+
return unless earliest_date || latest_date
|
|
350
344
|
|
|
351
|
-
earliest_date
|
|
345
|
+
start = earliest_date || latest_date
|
|
346
|
+
stop = latest_date || ::Date.today
|
|
347
|
+
|
|
348
|
+
start..stop
|
|
352
349
|
end
|
|
353
350
|
|
|
354
|
-
# Array of all
|
|
355
|
-
# @note
|
|
356
|
-
# @
|
|
351
|
+
# Array of all individual {Date}s that are described by the data.
|
|
352
|
+
# @note Output dates will have the same precision as the input date (e.g. year vs day).
|
|
353
|
+
# @note If the range is open-ended, uses today's date as the end date.
|
|
354
|
+
# @note {EDTF::Set}s can be disjoint ranges; unlike {#as_range} this method will respect any gaps.
|
|
355
|
+
# @return [Array<Date>]
|
|
357
356
|
def to_a
|
|
358
357
|
case date
|
|
359
358
|
when EDTF::Set
|
|
@@ -363,8 +362,6 @@ module CocinaDisplay
|
|
|
363
362
|
end
|
|
364
363
|
end
|
|
365
364
|
|
|
366
|
-
private
|
|
367
|
-
|
|
368
365
|
class << self
|
|
369
366
|
# Returns the date in the format specified by the precision.
|
|
370
367
|
# Supports e.g. retrieving year precision when the actual date is more precise.
|
|
@@ -444,6 +441,8 @@ module CocinaDisplay
|
|
|
444
441
|
end
|
|
445
442
|
end
|
|
446
443
|
|
|
444
|
+
private
|
|
445
|
+
|
|
447
446
|
# Expand placeholders like "19XX" into an object representing the full range.
|
|
448
447
|
# @note This is different from dates with an explicit start/end in the Cocina.
|
|
449
448
|
# @see CocinaDisplay::Dates::DateRange
|
|
@@ -523,13 +522,11 @@ module CocinaDisplay
|
|
|
523
522
|
# MARC date parser; similar to EDTF but with some MARC-specific encodings.
|
|
524
523
|
class MarcFormat < Date
|
|
525
524
|
def self.normalize_to_edtf(value)
|
|
526
|
-
return nil if value == "9999" || value == "
|
|
525
|
+
return nil if value == "9999" || value == "||||"
|
|
527
526
|
|
|
528
527
|
super
|
|
529
528
|
end
|
|
530
529
|
|
|
531
|
-
private
|
|
532
|
-
|
|
533
530
|
def earliest_date
|
|
534
531
|
if value == "1uuu"
|
|
535
532
|
::Date.parse("1000-01-01")
|
|
@@ -545,6 +542,8 @@ module CocinaDisplay
|
|
|
545
542
|
super
|
|
546
543
|
end
|
|
547
544
|
end
|
|
545
|
+
|
|
546
|
+
private
|
|
548
547
|
end
|
|
549
548
|
|
|
550
549
|
# Base class for date formats that match using a regex.
|
|
@@ -600,7 +599,7 @@ module CocinaDisplay
|
|
|
600
599
|
|
|
601
600
|
# Extractor for dates encoded as Roman numerals.
|
|
602
601
|
class RomanNumeralYearFormat < ExtractorDateFormat
|
|
603
|
-
REGEX = /(?<![A-Za-z
|
|
602
|
+
REGEX = /(?<![A-Za-z.])(?<year>[MCDLXVI.]+)(?![A-Za-z])/
|
|
604
603
|
|
|
605
604
|
def self.normalize_to_edtf(text)
|
|
606
605
|
matches = text.match(REGEX)
|
|
@@ -14,8 +14,8 @@ module CocinaDisplay
|
|
|
14
14
|
# Create the individual dates; if no encoding/type declared give them
|
|
15
15
|
# top-level encoding/type
|
|
16
16
|
dates = cocina["structuredValue"].map do |sv|
|
|
17
|
+
sv["encoding"] ||= cocina["encoding"]
|
|
17
18
|
date = Date.from_cocina(sv)
|
|
18
|
-
date.encoding ||= cocina.dig("encoding", "code")
|
|
19
19
|
date.type ||= cocina["type"]
|
|
20
20
|
date
|
|
21
21
|
end
|
|
@@ -137,6 +137,20 @@ module CocinaDisplay
|
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
# Earliest possible date encoded in data, respecting unspecified/imprecise info.
|
|
141
|
+
# @return [Date]
|
|
142
|
+
# @return [nil] if open start
|
|
143
|
+
def earliest_date
|
|
144
|
+
start&.earliest_date
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Latest possible date encoded in data, respecting unspecified/imprecise info.
|
|
148
|
+
# @return [Date]
|
|
149
|
+
# @return [nil] if open-ended range
|
|
150
|
+
def latest_date
|
|
151
|
+
stop&.latest_date
|
|
152
|
+
end
|
|
153
|
+
|
|
140
154
|
# Express the range as an EDTF::Interval between the start and stop dates.
|
|
141
155
|
# @return [EDTF::Interval]
|
|
142
156
|
def as_interval
|
|
@@ -144,6 +158,20 @@ module CocinaDisplay
|
|
|
144
158
|
interval_stop = stop&.date&.edtf || "open"
|
|
145
159
|
::Date.edtf("#{interval_start}/#{interval_stop}")
|
|
146
160
|
end
|
|
161
|
+
|
|
162
|
+
# Array of all individual {Date}s that are described by the data.
|
|
163
|
+
# @note Output dates will have the same precision as the input date (e.g. year vs day).
|
|
164
|
+
# @note {EDTF::Set}s can be disjoint ranges; unlike {#as_range} this method will respect any gaps.
|
|
165
|
+
# @return [Array<Date>]
|
|
166
|
+
def to_a
|
|
167
|
+
start_dates = start&.to_a || []
|
|
168
|
+
stop_dates = stop&.to_a || []
|
|
169
|
+
|
|
170
|
+
return [] if start_dates.empty? && stop_dates.empty?
|
|
171
|
+
return as_range.to_a if start_dates.one? && stop_dates.one? || stop_dates.empty?
|
|
172
|
+
|
|
173
|
+
[start_dates, stop_dates].flatten.sort.uniq
|
|
174
|
+
end
|
|
147
175
|
end
|
|
148
176
|
end
|
|
149
177
|
end
|
|
@@ -2,10 +2,19 @@ module CocinaDisplay
|
|
|
2
2
|
module Forms
|
|
3
3
|
# A Resource Type form associated with part or all of a Cocina object.
|
|
4
4
|
class ResourceType < Form
|
|
5
|
-
# Resource types are lowercased for display.
|
|
5
|
+
# Resource types are lowercased for display, except self-deposit types.
|
|
6
6
|
# @return [String]
|
|
7
7
|
def to_s
|
|
8
|
-
|
|
8
|
+
stanford_self_deposit? ? flat_value : flat_value.downcase
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# For self-deposit resource types, the flat value comprises primary and any subtypes.
|
|
12
|
+
# @return [String]
|
|
13
|
+
def flat_value
|
|
14
|
+
return super unless stanford_self_deposit?
|
|
15
|
+
return primary_type unless subtypes.any?
|
|
16
|
+
|
|
17
|
+
"#{primary_type} (#{subtypes.join(", ")})"
|
|
9
18
|
end
|
|
10
19
|
|
|
11
20
|
# Is this a Stanford self-deposit resource type?
|
|
@@ -33,6 +42,30 @@ module CocinaDisplay
|
|
|
33
42
|
def type_label
|
|
34
43
|
(I18n.t("cocina_display.field_label.form.genre") if stanford_self_deposit?) || super
|
|
35
44
|
end
|
|
45
|
+
|
|
46
|
+
# The primary type, if this is a structured self-deposit resource type.
|
|
47
|
+
# @return [String, nil]
|
|
48
|
+
def primary_type
|
|
49
|
+
type_components["type"].first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The subtypes, if this is a structured self-deposit resource type.
|
|
53
|
+
# @return [Array<String>]
|
|
54
|
+
def subtypes
|
|
55
|
+
type_components["subtype"] || []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A hash containing the destructured resource type and subtypes, if any.
|
|
59
|
+
# @return [Hash<String, Array<String>>]
|
|
60
|
+
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#form-part-types-for-structured-value
|
|
61
|
+
# @note Only used by self-deposit resource types.
|
|
62
|
+
def type_components
|
|
63
|
+
Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
|
|
64
|
+
type = node["type"]
|
|
65
|
+
hash[type] ||= []
|
|
66
|
+
hash[type] << node["value"]
|
|
67
|
+
end.compact_blank
|
|
68
|
+
end
|
|
36
69
|
end
|
|
37
70
|
end
|
|
38
71
|
end
|
|
@@ -38,7 +38,7 @@ module CocinaDisplay
|
|
|
38
38
|
# @return [Coordinates, nil]
|
|
39
39
|
def parse(value)
|
|
40
40
|
# Remove all whitespace for easier matching/parsing
|
|
41
|
-
match_str = value.gsub(
|
|
41
|
+
match_str = value.gsub(/\s+/, "")
|
|
42
42
|
|
|
43
43
|
# Try each parser in order until one matches; bail out if none do
|
|
44
44
|
parser_class = [
|
|
@@ -310,12 +310,14 @@ module CocinaDisplay
|
|
|
310
310
|
# Parse for decimal degree points, like "41.891797, 12.486419".
|
|
311
311
|
class DecimalPointParser < PointParser
|
|
312
312
|
include DecimalParser
|
|
313
|
-
|
|
313
|
+
|
|
314
|
+
PATTERN = /(?<lat>[0-9.EW+-]+),(?<lng>[0-9.NS+-]+)/
|
|
314
315
|
end
|
|
315
316
|
|
|
316
317
|
# Parser for DMS-format points, like "N34°03′08″ W118°14′37″".
|
|
317
318
|
class DMSPointParser < PointParser
|
|
318
319
|
include DMSParser
|
|
320
|
+
|
|
319
321
|
PATTERN = /(?<lat>[^EW]+)(?<lng>[^NS]+)/
|
|
320
322
|
end
|
|
321
323
|
|
|
@@ -324,6 +326,7 @@ module CocinaDisplay
|
|
|
324
326
|
# @see https://www.oclc.org/bibformats/en/2xx/255.html#subfieldc
|
|
325
327
|
class DMSBoundingBoxParser < BoundingBoxParser
|
|
326
328
|
include DMSParser
|
|
329
|
+
|
|
327
330
|
PATTERN = /(?<min_lng>.+?)-+(?<max_lng>.+)\/(?<max_lat>.+?)-+(?<min_lat>.+)/
|
|
328
331
|
end
|
|
329
332
|
|
|
@@ -331,6 +334,7 @@ module CocinaDisplay
|
|
|
331
334
|
# @example W 126.04--W 052.03/N 050.37--N 006.8
|
|
332
335
|
class DecimalBoundingBoxParser < BoundingBoxParser
|
|
333
336
|
include DecimalParser
|
|
337
|
+
|
|
334
338
|
PATTERN = /(?<min_lng>[0-9.EW]+?)-+(?<max_lng>[0-9.EW]+)\/(?<max_lat>[0-9.NS]+?)-+(?<min_lat>[0-9.NS]+)/
|
|
335
339
|
end
|
|
336
340
|
|
|
@@ -8,7 +8,10 @@ module CocinaDisplay
|
|
|
8
8
|
attr_reader :cocina_doc
|
|
9
9
|
|
|
10
10
|
# Initialize a record with a Cocina document hash.
|
|
11
|
-
# @param cocina_doc [Hash]
|
|
11
|
+
# @param cocina_doc [Hash<String, Object>] The Cocina document hash
|
|
12
|
+
# @example Initialize a record with a Cocina document
|
|
13
|
+
# cocina = Cocina.find(id)
|
|
14
|
+
# record = CocinaDisplay::CocinaDisplay.new(cocina.as_json)
|
|
12
15
|
def initialize(cocina_doc)
|
|
13
16
|
@cocina_doc = cocina_doc
|
|
14
17
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module CocinaDisplay
|
|
2
|
+
module Structural
|
|
3
|
+
# Represents a single file in a Cocina object.
|
|
4
|
+
class File
|
|
5
|
+
# Underlying hash parsed from Cocina JSON.
|
|
6
|
+
attr_reader :cocina
|
|
7
|
+
|
|
8
|
+
# URL to Stacks environment that will serve this file.
|
|
9
|
+
attr_reader :base_url
|
|
10
|
+
|
|
11
|
+
# Initialize the File with Cocina file data.
|
|
12
|
+
# @param cocina [Hash] Cocina structured data for a single file
|
|
13
|
+
# @param druid [String, nil] DRUID of the object this file belongs to
|
|
14
|
+
# @note Staging objects can't infer their DRUID and need it passed in explicitly.
|
|
15
|
+
def initialize(cocina, base_url: "https://stacks.stanford.edu", druid: nil)
|
|
16
|
+
@cocina = cocina
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@druid = druid
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The name of the file on disk, including file extension.
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
# @example "bc798xr9549_30C_Kalsang_Yulgial_thumb.jp2"
|
|
24
|
+
def filename
|
|
25
|
+
cocina["filename"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The MIME type of the file.
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
# @example "image/jp2"
|
|
31
|
+
def mime_type
|
|
32
|
+
cocina["hasMimeType"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The relation of the file to the object.
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
# @example "thumbnail"
|
|
38
|
+
def use
|
|
39
|
+
cocina["use"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The size in bytes of the file.
|
|
43
|
+
# @return [Integer, nil]
|
|
44
|
+
# @example 204800
|
|
45
|
+
def size
|
|
46
|
+
cocina["size"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# True if this file was marked as a thumbnail and has nonzero dimensions.
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def thumbnail?
|
|
52
|
+
use == "thumbnail" && nonzero_dimensions?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# True if this file is a JP2 image and has nonzero dimensions.
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def jp2_image?
|
|
58
|
+
mime_type == "image/jp2" && nonzero_dimensions?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# True if file is an image with nonzero height and width.
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def nonzero_dimensions?
|
|
64
|
+
height&.positive? && width&.positive?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The height of the image in pixels, if applicable.
|
|
68
|
+
# @return [Integer, nil]
|
|
69
|
+
def height
|
|
70
|
+
cocina.dig("presentation", "height").to_i
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The width of the image in pixels, if applicable.
|
|
74
|
+
# @return [Integer, nil]
|
|
75
|
+
def width
|
|
76
|
+
cocina.dig("presentation", "width").to_i
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Generate a IIIF image URL for this file.
|
|
80
|
+
# @param region [String] Desired region of the image (e.g., "full", "square", "x,y,w,h", "pct:x,y,w,h").
|
|
81
|
+
# @param width [String] Desired width of the image in pixels (use "!" prefix to preserve aspect ratio).
|
|
82
|
+
# @param height [String] Desired height of the image in pixels.
|
|
83
|
+
# @return [String, nil]
|
|
84
|
+
# @example "https://stacks.stanford.edu/image/iiif/ts786ny5936%2FPC0170_s1_E_0204.jp2/full/!400,400/0/default.jpg"
|
|
85
|
+
def iiif_url(region: "full", width: "!400", height: "400")
|
|
86
|
+
return unless iiif_id.present?
|
|
87
|
+
|
|
88
|
+
"#{base_url}/image/iiif/#{iiif_id}/#{region}/#{width},#{height}/0/default.jpg"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# For images served over IIIF, we use the encoded file ID minus the extension.
|
|
92
|
+
# @return [String, nil]
|
|
93
|
+
# @example "ts786ny5936%2FPC0170_s1_E_0204"
|
|
94
|
+
def iiif_id
|
|
95
|
+
ERB::Util.url_encode(file_id.delete_suffix(".jp2")) if file_id.present? && jp2_image?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Generate a download URL for this file from stacks.
|
|
99
|
+
# @return [String, nil]
|
|
100
|
+
def download_url
|
|
101
|
+
return unless file_id.present?
|
|
102
|
+
|
|
103
|
+
"#{base_url}/file/druid:#{file_id}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# External identifier for the file, minus the URL prefix.
|
|
109
|
+
# @return [String, nil]
|
|
110
|
+
# @note Staging and production formats differ.
|
|
111
|
+
# @example production
|
|
112
|
+
# "fn851zf9475-fn851zf9475_1/fn851zf9475_00_0001.jp2"
|
|
113
|
+
# @example staging
|
|
114
|
+
# "ddbd323d-0dd9-4f14-ba72-336c2bccfb29"
|
|
115
|
+
def external_id
|
|
116
|
+
cocina["externalIdentifier"]&.delete_prefix("https://cocina.sul.stanford.edu/file/")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The DRUID of the object this file belongs to.
|
|
120
|
+
# @note Staging objects can't infer this from the externalIdentifier.
|
|
121
|
+
# @return [String, nil]
|
|
122
|
+
def druid
|
|
123
|
+
@druid || external_id.split("-").first if external_id.present?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Combination of the DRUID and filename to uniquely identify the file.
|
|
127
|
+
# @return [String, nil]
|
|
128
|
+
# @example "ts786ny5936/PC0170_s1_E_0204.jp2"
|
|
129
|
+
def file_id
|
|
130
|
+
"#{druid}/#{filename}" if druid.present? && filename.present?
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module CocinaDisplay
|
|
2
|
+
module Structural
|
|
3
|
+
# Represents a set of files in a Cocina object.
|
|
4
|
+
class FileSet
|
|
5
|
+
# Underlying hash parsed from Cocina JSON.
|
|
6
|
+
attr_reader :cocina
|
|
7
|
+
|
|
8
|
+
# URL to Stacks environment that will serve this fileset.
|
|
9
|
+
attr_reader :base_url
|
|
10
|
+
|
|
11
|
+
# Initialize the FileSet with Cocina structural data.
|
|
12
|
+
# @param cocina [Hash] Cocina structured data for a single FileSet
|
|
13
|
+
# @param base_url [String] URL to Stacks environment that will serve this fileset
|
|
14
|
+
# @param druid [String, nil] DRUID of the object this fileset belongs to
|
|
15
|
+
def initialize(cocina, base_url: "https://stacks.stanford.edu", druid: nil)
|
|
16
|
+
@cocina = cocina
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@druid = druid
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The declared type of the FileSet, like "image" or "document".
|
|
22
|
+
# @note This can differ from the contained file types.
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
def type
|
|
25
|
+
cocina["type"]&.delete_prefix("https://cocina.sul.stanford.edu/models/resources/")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# All files contained in this FileSet.
|
|
29
|
+
# @return [Array<CocinaDisplay::Structural::File>]
|
|
30
|
+
def files
|
|
31
|
+
@files ||= Array(cocina.dig("structural", "contains")).map do |file|
|
|
32
|
+
CocinaDisplay::Structural::File.new(file, base_url: base_url, druid: druid)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# DRUID of the object this fileset belongs to.
|
|
39
|
+
# @note Inferred from the start of the externalIdentifier.
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
def druid
|
|
42
|
+
@druid || external_id[/^[a-z]{2}\d{3}[a-z]{2}\d{4}/] if external_id.present?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# External identifier for the fileset, minus the URL prefix.
|
|
46
|
+
# @return [String, nil]
|
|
47
|
+
# @note Staging and production formats differ.
|
|
48
|
+
# @example production
|
|
49
|
+
# "bk264hq9320-bk264hq9320_3"
|
|
50
|
+
# @example staging
|
|
51
|
+
# "bh114dk3076_4"
|
|
52
|
+
def external_id
|
|
53
|
+
cocina["externalIdentifier"]&.delete_prefix("https://cocina.sul.stanford.edu/fileSet/")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/cocina_display/title.rb
CHANGED
|
@@ -109,7 +109,7 @@ module CocinaDisplay
|
|
|
109
109
|
# Generate the display title by stripping trailing punctuation from the full title.
|
|
110
110
|
# @return [String, nil]
|
|
111
111
|
def display_title_str
|
|
112
|
-
full_title_str&.sub(/[
|
|
112
|
+
full_title_str&.sub(/[.,;:\/\\]+\z/, "")
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
# The main title and subtitle, joined together with a colon.
|
data/lib/cocina_display/utils.rb
CHANGED
|
@@ -13,12 +13,12 @@ module CocinaDisplay
|
|
|
13
13
|
return compacted_values.first if compacted_values.one?
|
|
14
14
|
|
|
15
15
|
compacted_values.reduce(+"") do |result, value|
|
|
16
|
-
result << if value.end_with?(delimiter.strip)
|
|
16
|
+
result << if value.end_with?(delimiter.strip) || value.start_with?(delimiter.strip)
|
|
17
17
|
value + " "
|
|
18
18
|
else
|
|
19
19
|
value + delimiter
|
|
20
20
|
end
|
|
21
|
-
end.delete_suffix(delimiter)
|
|
21
|
+
end.delete_prefix(delimiter).delete_suffix(delimiter).strip
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Recursively flatten structured, and grouped values in Cocina metadata.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cocina_display
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Budak
|
|
@@ -268,6 +268,8 @@ files:
|
|
|
268
268
|
- lib/cocina_display/license.rb
|
|
269
269
|
- lib/cocina_display/note.rb
|
|
270
270
|
- lib/cocina_display/related_resource.rb
|
|
271
|
+
- lib/cocina_display/structural/file.rb
|
|
272
|
+
- lib/cocina_display/structural/file_set.rb
|
|
271
273
|
- lib/cocina_display/subjects/subject.rb
|
|
272
274
|
- lib/cocina_display/subjects/subject_value.rb
|
|
273
275
|
- lib/cocina_display/title.rb
|
|
@@ -296,7 +298,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
296
298
|
- !ruby/object:Gem::Version
|
|
297
299
|
version: '0'
|
|
298
300
|
requirements: []
|
|
299
|
-
rubygems_version: 4.0.
|
|
301
|
+
rubygems_version: 4.0.3
|
|
300
302
|
specification_version: 4
|
|
301
303
|
summary: Helpers for rendering Cocina metadata
|
|
302
304
|
test_files: []
|