neu-mods 0.8.0 → 0.10.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/.version +1 -1
- data/README.md +8 -1
- data/lib/neu/mods/projection.rb +209 -12
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa2cba0771dfcd203ae956c9f5a84cb58c004df684620e378bf4034b978ee6b3
|
|
4
|
+
data.tar.gz: e3610d3984fecaebccdb88032ab07b4fab55b0e6932ee5f54218b654016ceca5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bcbcf544da159f6e2377ae868808e73e78d254e9c597e2dcd676d140cbb6f59a8624ca698233bd0ab18866c9438ddae988a7fc0ac485ce7fa3557c34d199a76
|
|
7
|
+
data.tar.gz: de478ac9c203c1edf05fb604c9da85b05e95e133fb20231716a6ef7dd9310292dacf753f13781d253ecbd93a04aa290e2eff40dc32c5331de0e278c1b07c736f
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
data/README.md
CHANGED
|
@@ -50,6 +50,13 @@ doc.related_items # => [{ type: "otherFormat", title: "..." }, ...]
|
|
|
50
50
|
# every relatedItem that is not a series or a host
|
|
51
51
|
doc.location # => [{ physical_location:, shelf_location:, url: }, ...]
|
|
52
52
|
doc.map_data # => [{ scale:, projection:, coordinates: }, ...]
|
|
53
|
+
doc.title_subjects # => ["The Great Gatsby"] composed like the main title
|
|
54
|
+
doc.hierarchical_geographic_subjects
|
|
55
|
+
# => [{ country:, state:, city:, ... }, ...] eleven levels,
|
|
56
|
+
# structured for the reason map_data is
|
|
57
|
+
doc.record_info # => { content_source:, origin:, description_standard:,
|
|
58
|
+
# creation_date:, change_date:, language_of_cataloging: }
|
|
59
|
+
# describes the CATALOGUING, not the resource
|
|
53
60
|
doc.to_h # => full projection, keyed to Atlas's Metadata::MODS attributes
|
|
54
61
|
|
|
55
62
|
# The field registry -- the single declaration of what this gem projects.
|
|
@@ -73,7 +80,7 @@ doc.to_xml
|
|
|
73
80
|
# node selection (for replace-on-save), and structure-aware build.
|
|
74
81
|
doc.editable_personal_creators # => [{ given:, family: }] (plain, Creator role)
|
|
75
82
|
doc.editable_corporate_creators # => [{ name: }]
|
|
76
|
-
doc.preserved_names # => [{ name:,
|
|
83
|
+
doc.preserved_names # => [{ name:, roles: }] (authority-bearing / non-Creator — read-only)
|
|
77
84
|
doc.editable_creator_nodes("personal") # => live <name> nodes to replace
|
|
78
85
|
doc.build_personal_name(given: "Jenny", family: "Smith") # => a plain personal <name> node
|
|
79
86
|
doc.build_corporate_name(name: "Northeastern University") # => a plain corporate <name> node
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -114,6 +114,26 @@ module NEU
|
|
|
114
114
|
keyword_subjects.flat_map { |s| s.xpath("mods:topic", NAMESPACE).map { |t| t.text.strip } }
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
# Neither child carries heading text: cartographics is a structured
|
|
118
|
+
# coordinate a reader reaches through #map_data, and geographicCode is a
|
|
119
|
+
# MARC code rather than a place name.
|
|
120
|
+
HEADING_OMITTED_CHILDREN = %w[cartographics geographicCode].freeze
|
|
121
|
+
|
|
122
|
+
# Every top-level <subject> as ONE heading, its parts in document order.
|
|
123
|
+
# A pre-coordinated heading like "Salt marshes--Massachusetts--20th
|
|
124
|
+
# century" is a single statement, and the per-axis fields below cannot say
|
|
125
|
+
# which parts belonged together: they pool every topic on the record into
|
|
126
|
+
# one list, so a fragment of a heading and a whole heading read alike.
|
|
127
|
+
#
|
|
128
|
+
# Kept in parts rather than joined. The separator is display policy, the
|
|
129
|
+
# same call #map_data makes for cartographics.
|
|
130
|
+
def subject_headings
|
|
131
|
+
doc.xpath("/mods:mods/mods:subject", NAMESPACE).filter_map do |node|
|
|
132
|
+
parts = subject_heading_parts(node)
|
|
133
|
+
{ parts: parts } unless parts.empty?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
117
137
|
# Every <topic> under any top-level <subject> (the access-copy projection,
|
|
118
138
|
# equivalent to Atlas's extract_topical_subjects).
|
|
119
139
|
def topical_subjects = texts_at("/mods:mods/mods:subject/mods:topic")
|
|
@@ -130,6 +150,39 @@ module NEU
|
|
|
130
150
|
def personal_name_subjects = name_subjects("personal")
|
|
131
151
|
def corporate_name_subjects = name_subjects("corporate")
|
|
132
152
|
|
|
153
|
+
# The last unprojected member of a closed set: every other subject child
|
|
154
|
+
# already has a field, so leaving this one out made "what a subject can
|
|
155
|
+
# carry" arbitrary rather than complete.
|
|
156
|
+
def occupation_subjects = texts_at("/mods:mods/mods:subject/mods:occupation")
|
|
157
|
+
|
|
158
|
+
def genre_subjects = texts_at("/mods:mods/mods:subject/mods:genre")
|
|
159
|
+
|
|
160
|
+
# A MARC GAC code. Projected as the record wrote it: turning it into a
|
|
161
|
+
# place name needs a lookup table, which is the same call the gem already
|
|
162
|
+
# made for MARC relators -- the label vocabulary belongs to the consumer.
|
|
163
|
+
def geographic_code_subjects = texts_at("/mods:mods/mods:subject/mods:geographicCode")
|
|
164
|
+
|
|
165
|
+
# A subject that is a work has a nonSort, a subTitle and part numbers like
|
|
166
|
+
# any other titleInfo, so it composes through the same port as the main
|
|
167
|
+
# title rather than taking titleInfo/title alone.
|
|
168
|
+
def title_subjects
|
|
169
|
+
doc.xpath("/mods:mods/mods:subject/mods:titleInfo", NAMESPACE).filter_map { |node| composed_title_of(node) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Kept structured for the reason #map_data is. Flattening country / state
|
|
173
|
+
# / city into "United States -- New York (State) -- Parksville" would make
|
|
174
|
+
# a consumer that wants the city alone unpick a sentence.
|
|
175
|
+
#
|
|
176
|
+
# This is the axis bdr_43888.mods.xml uses INSTEAD of subject/geographic,
|
|
177
|
+
# so that record projected no place at all -- a live ingest path, not a
|
|
178
|
+
# hypothetical.
|
|
179
|
+
def hierarchical_geographic_subjects
|
|
180
|
+
doc.xpath("/mods:mods/mods:subject/mods:hierarchicalGeographic", NAMESPACE).filter_map do |node|
|
|
181
|
+
entry = HIERARCHICAL_GEOGRAPHIC_LEVELS.to_h { |level| [level, child_text(node, "mods:#{camelize(level)}")] }
|
|
182
|
+
entry if entry.values.any?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
133
186
|
# --- Names ---------------------------------------------------------------
|
|
134
187
|
|
|
135
188
|
# One name as the access copy wants it. `affiliation` is how a reader
|
|
@@ -141,15 +194,17 @@ module NEU
|
|
|
141
194
|
def name_entry(node)
|
|
142
195
|
{
|
|
143
196
|
name: name_display_value_w_date(node),
|
|
144
|
-
|
|
197
|
+
roles: name_roles(node),
|
|
145
198
|
affiliation: texts_under(node, "mods:affiliation")
|
|
146
199
|
}
|
|
147
200
|
end
|
|
148
201
|
|
|
149
|
-
# All top-level names as { name:,
|
|
202
|
+
# All top-level names as { name:, roles: }. `name` reproduces the `mods` gem's
|
|
150
203
|
# display_value_w_date (including its quirks -- faithfully, so existing Solr/
|
|
151
|
-
# display output is preserved). `role`
|
|
152
|
-
#
|
|
204
|
+
# display output is preserved). MODS repeats `role` on one name, and a
|
|
205
|
+
# cataloguer who records that a person both wrote and edited a work means
|
|
206
|
+
# both. Each term prefers the type="text" roleTerm, falling back to the raw
|
|
207
|
+
# code (NOT MARC-relator-translated -- see README).
|
|
153
208
|
def names
|
|
154
209
|
doc.xpath("/mods:mods/mods:name", NAMESPACE).map { |node| name_entry(node) }
|
|
155
210
|
end
|
|
@@ -169,7 +224,7 @@ module NEU
|
|
|
169
224
|
|
|
170
225
|
# Names the editable form does NOT manage (authority-bearing or non-Creator)
|
|
171
226
|
# -- for read-only display ("these exist; edit via the XML tab"). Composed
|
|
172
|
-
# display string +
|
|
227
|
+
# display string + roles, like #names but filtered to the preserved set.
|
|
173
228
|
def preserved_names
|
|
174
229
|
doc.xpath("/mods:mods/mods:name", NAMESPACE)
|
|
175
230
|
.reject { |node| editable_creator_name?(node) }
|
|
@@ -208,6 +263,30 @@ module NEU
|
|
|
208
263
|
# back by nothing.
|
|
209
264
|
def publication_information = texts_at("/mods:mods/mods:originInfo/mods:publisher")
|
|
210
265
|
def edition = texts_at("/mods:mods/mods:originInfo/mods:edition")
|
|
266
|
+
def place_of_publication = texts_at("/mods:mods/mods:originInfo/mods:place/mods:placeTerm")
|
|
267
|
+
def issuance = texts_at("/mods:mods/mods:originInfo/mods:issuance")
|
|
268
|
+
|
|
269
|
+
# Serials. The @authority a record puts on a frequency is not projected:
|
|
270
|
+
# authority handling is a question the gem defers everywhere else -- for
|
|
271
|
+
# genre, subject and name -- and answering it for one field would be
|
|
272
|
+
# inconsistent.
|
|
273
|
+
def frequency = texts_at("/mods:mods/mods:originInfo/mods:frequency")
|
|
274
|
+
|
|
275
|
+
def table_of_contents = texts_at("/mods:mods/mods:tableOfContents")
|
|
276
|
+
def reformatting_quality = texts_at("/mods:mods/mods:physicalDescription/mods:reformattingQuality")
|
|
277
|
+
|
|
278
|
+
# A note about the object rather than about the work -- "Scanned at 600
|
|
279
|
+
# dpi" belongs beside the extent, not beside a content note. Projected as
|
|
280
|
+
# plain strings like its physicalDescription siblings: #notes keeps @type
|
|
281
|
+
# because the type changes what a top-level note means, and nothing here
|
|
282
|
+
# turns on it.
|
|
283
|
+
def physical_description_notes = texts_at("/mods:mods/mods:physicalDescription/mods:note")
|
|
284
|
+
|
|
285
|
+
# An LCC or DDC call number. Note this is NOT the same concept as Atlas's
|
|
286
|
+
# classification_ssim, which carries a FileSet content-type vocabulary --
|
|
287
|
+
# the name collision is accidental and the consumer has to pick a free
|
|
288
|
+
# Solr field.
|
|
289
|
+
def classification = texts_at("/mods:mods/mods:classification")
|
|
211
290
|
|
|
212
291
|
# Every top-level note, keeping its @type. The type carries meaning -- a
|
|
213
292
|
# "statement of responsibility" is not a "funding" note -- so flattening
|
|
@@ -253,7 +332,18 @@ module NEU
|
|
|
253
332
|
end
|
|
254
333
|
|
|
255
334
|
def related_series = related_item_titles("series")
|
|
256
|
-
|
|
335
|
+
|
|
336
|
+
# The host's title plus THIS work's position within it. The host's own
|
|
337
|
+
# name, originInfo and identifier stay out: they belong to the other
|
|
338
|
+
# record, and a transcribed copy goes stale the moment that record is
|
|
339
|
+
# edited. A part is the exception, because a volume, issue and page range
|
|
340
|
+
# describe this article and no other record holds that fact.
|
|
341
|
+
def host_collections
|
|
342
|
+
doc.xpath("/mods:mods/mods:relatedItem[@type='host']", NAMESPACE).filter_map do |node|
|
|
343
|
+
title = child_text(node, "mods:titleInfo/mods:title")
|
|
344
|
+
{ title: title, **host_part(node) } if title
|
|
345
|
+
end
|
|
346
|
+
end
|
|
257
347
|
|
|
258
348
|
# relatedItem @type values that already have a field of their own, so the
|
|
259
349
|
# catch-all below does not repeat them.
|
|
@@ -297,6 +387,26 @@ module NEU
|
|
|
297
387
|
# or a full date. Matching the shape explicitly, rather than widening
|
|
298
388
|
# DateTime.parse, is what lets the declared precision fall out of the parse
|
|
299
389
|
# instead of being guessed after it.
|
|
390
|
+
# The eleven children the XSD allows under hierarchicalGeographic, in the
|
|
391
|
+
# order MODS lists them -- broadest first, which is also the order a
|
|
392
|
+
# consumer composing a place string wants to reverse.
|
|
393
|
+
HIERARCHICAL_GEOGRAPHIC_LEVELS = %i[
|
|
394
|
+
continent country province region state territory county city
|
|
395
|
+
city_section island area
|
|
396
|
+
].freeze
|
|
397
|
+
|
|
398
|
+
# recordInfo children. Read as a single value: the schema repeats the
|
|
399
|
+
# element, but a record with two cataloguing provenances is not a case
|
|
400
|
+
# anyone has, and an array here buys nothing.
|
|
401
|
+
RECORD_INFO_PARTS = {
|
|
402
|
+
content_source: "mods:recordContentSource",
|
|
403
|
+
origin: "mods:recordOrigin",
|
|
404
|
+
description_standard: "mods:descriptionStandard",
|
|
405
|
+
creation_date: "mods:recordCreationDate",
|
|
406
|
+
change_date: "mods:recordChangeDate",
|
|
407
|
+
language_of_cataloging: "mods:languageOfCataloging/mods:languageTerm"
|
|
408
|
+
}.freeze
|
|
409
|
+
|
|
300
410
|
W3CDTF_DATE = /\A(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?\z/
|
|
301
411
|
|
|
302
412
|
# What #date_parts returns when the element is absent entirely, so an
|
|
@@ -351,6 +461,19 @@ module NEU
|
|
|
351
461
|
def date_issued_with_precision = [date_issued, date_issued_precision]
|
|
352
462
|
def copyright_date_with_precision = [copyright_date, copyright_date_precision]
|
|
353
463
|
|
|
464
|
+
# Who catalogued this record, to what standard, and when. It describes the
|
|
465
|
+
# CATALOGUING rather than the resource, which is why it is one value and
|
|
466
|
+
# why a consumer is unlikely to want it beside Publisher -- but dropping a
|
|
467
|
+
# preservation repository's provenance statement on read is wrong on its
|
|
468
|
+
# face, so it is projected and the display question is the consumer's.
|
|
469
|
+
def record_info
|
|
470
|
+
node = doc.at_xpath("/mods:mods/mods:recordInfo", NAMESPACE)
|
|
471
|
+
return nil unless node
|
|
472
|
+
|
|
473
|
+
entry = RECORD_INFO_PARTS.transform_values { |xpath| child_text(node, xpath) }
|
|
474
|
+
entry if entry.values.any?
|
|
475
|
+
end
|
|
476
|
+
|
|
354
477
|
# --- Full projection -----------------------------------------------------
|
|
355
478
|
|
|
356
479
|
# The field registry: the single declaration of what this gem projects.
|
|
@@ -379,7 +502,10 @@ module NEU
|
|
|
379
502
|
|
|
380
503
|
# origin
|
|
381
504
|
publication_information: :many,
|
|
505
|
+
place_of_publication: :many,
|
|
382
506
|
edition: :many,
|
|
507
|
+
issuance: :many,
|
|
508
|
+
frequency: :many,
|
|
383
509
|
# Six rows per originInfo date. Flat rather than one nested value,
|
|
384
510
|
# because the value half has consumers that need a real date object.
|
|
385
511
|
date_created: :one,
|
|
@@ -407,14 +533,23 @@ module NEU
|
|
|
407
533
|
format: :many,
|
|
408
534
|
extent: :many,
|
|
409
535
|
digital_origin: :many,
|
|
536
|
+
reformatting_quality: :many,
|
|
537
|
+
physical_description_notes: :many,
|
|
410
538
|
notes: :many,
|
|
539
|
+
table_of_contents: :many,
|
|
411
540
|
|
|
412
541
|
# subjects
|
|
542
|
+
subject_headings: :many,
|
|
413
543
|
topical_subjects: :many,
|
|
414
544
|
geographic_subjects: :many,
|
|
415
545
|
temporal_subjects: :many,
|
|
416
546
|
personal_name_subjects: :many,
|
|
417
547
|
corporate_name_subjects: :many,
|
|
548
|
+
occupation_subjects: :many,
|
|
549
|
+
genre_subjects: :many,
|
|
550
|
+
geographic_code_subjects: :many,
|
|
551
|
+
title_subjects: :many,
|
|
552
|
+
hierarchical_geographic_subjects: :many,
|
|
418
553
|
map_data: :many,
|
|
419
554
|
|
|
420
555
|
# related items
|
|
@@ -424,7 +559,9 @@ module NEU
|
|
|
424
559
|
|
|
425
560
|
# identifiers and location
|
|
426
561
|
identifiers: :many,
|
|
562
|
+
classification: :many,
|
|
427
563
|
permanent_url: :one,
|
|
564
|
+
record_info: :one,
|
|
428
565
|
location: :many,
|
|
429
566
|
|
|
430
567
|
# access
|
|
@@ -547,6 +684,13 @@ module NEU
|
|
|
547
684
|
# A variant title composed the way the access copy wants it: normalised
|
|
548
685
|
# first, like #access_title_parts, so a curly quote or an invisible format
|
|
549
686
|
# mark cannot reach Solr or a display template through this route either.
|
|
687
|
+
# One titleInfo composed the way the access copy wants it, shared by the
|
|
688
|
+
# subject-title axis and the assembled heading so the two cannot drift.
|
|
689
|
+
def composed_title_of(node)
|
|
690
|
+
parts = title_parts_of(node).transform_values { |value| NEU::MODS.normalize(value.to_s) }
|
|
691
|
+
clean(Projection.compose_title(parts))
|
|
692
|
+
end
|
|
693
|
+
|
|
550
694
|
def variant_titles(type)
|
|
551
695
|
doc.xpath("/mods:mods/mods:titleInfo[@type='#{type}']", NAMESPACE).filter_map do |node|
|
|
552
696
|
parts = title_parts_of(node).transform_values { |value| NEU::MODS.normalize(value.to_s) }
|
|
@@ -559,10 +703,54 @@ module NEU
|
|
|
559
703
|
.filter_map { |node| name_display_value_w_date(node) }
|
|
560
704
|
end
|
|
561
705
|
|
|
706
|
+
def subject_heading_parts(node)
|
|
707
|
+
node.xpath("mods:*", NAMESPACE).flat_map { |child| subject_heading_part(child) }.compact
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def subject_heading_part(child)
|
|
711
|
+
return [] if HEADING_OMITTED_CHILDREN.include?(child.name)
|
|
712
|
+
|
|
713
|
+
case child.name
|
|
714
|
+
when "name" then [name_display_value_w_date(child)]
|
|
715
|
+
when "titleInfo" then [composed_title_of(child)]
|
|
716
|
+
# Each level is its own part, so a hierarchical place reads as the steps
|
|
717
|
+
# of the heading rather than as one run-together string.
|
|
718
|
+
when "hierarchicalGeographic" then child.xpath("mods:*", NAMESPACE).map { |level| clean(level.text) }
|
|
719
|
+
else split_heading_text(clean(child.text))
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# A cataloguer who typed a whole heading into one element as "A--B--C"
|
|
724
|
+
# made the same statement as one who structured it into siblings, so both
|
|
725
|
+
# arrive here as the same parts.
|
|
726
|
+
def split_heading_text(text)
|
|
727
|
+
return [] if text.nil?
|
|
728
|
+
return [text] unless text.include?("--")
|
|
729
|
+
|
|
730
|
+
text.split("--").filter_map { |part| clean(part) }
|
|
731
|
+
end
|
|
732
|
+
|
|
562
733
|
def related_item_titles(type)
|
|
563
734
|
texts_at("/mods:mods/mods:relatedItem[@type='#{type}']/mods:titleInfo/mods:title")
|
|
564
735
|
end
|
|
565
736
|
|
|
737
|
+
# Kept in parts rather than composed into "24(3), pp. 210-218". The
|
|
738
|
+
# punctuation of a citation is display policy, the same call #map_data
|
|
739
|
+
# makes for cartographics. MODS leaves @unit optional, so a page extent
|
|
740
|
+
# without one is read rather than dropped.
|
|
741
|
+
def host_part(node)
|
|
742
|
+
part = node.at_xpath("mods:part", NAMESPACE)
|
|
743
|
+
return {} if part.nil?
|
|
744
|
+
|
|
745
|
+
pages = "mods:extent[@unit='page' or not(@unit)]"
|
|
746
|
+
{
|
|
747
|
+
volume: child_text(part, "mods:detail[@type='volume']/mods:number"),
|
|
748
|
+
issue: child_text(part, "mods:detail[@type='issue']/mods:number"),
|
|
749
|
+
start_page: child_text(part, "#{pages}/mods:start"),
|
|
750
|
+
end_page: child_text(part, "#{pages}/mods:end")
|
|
751
|
+
}.compact
|
|
752
|
+
end
|
|
753
|
+
|
|
566
754
|
def text_at(xpath)
|
|
567
755
|
node = doc.at_xpath(xpath, NAMESPACE)
|
|
568
756
|
node ? NEU::MODS.canonical_ws(node.text) : ""
|
|
@@ -575,6 +763,13 @@ module NEU
|
|
|
575
763
|
# [nil], and every consumer of that array has to guard for it.
|
|
576
764
|
# #texts_at scoped to a node rather than the document, for a repeatable
|
|
577
765
|
# child of one element.
|
|
766
|
+
# :city_section -> "citySection". The level names are snake_case in the
|
|
767
|
+
# projection and camelCase in the schema.
|
|
768
|
+
def camelize(level)
|
|
769
|
+
head, *rest = level.to_s.split("_")
|
|
770
|
+
[head, *rest.map(&:capitalize)].join
|
|
771
|
+
end
|
|
772
|
+
|
|
578
773
|
def texts_under(node, xpath)
|
|
579
774
|
node.xpath(xpath, NAMESPACE).filter_map { |child| clean(child.text) }
|
|
580
775
|
end
|
|
@@ -682,14 +877,16 @@ module NEU
|
|
|
682
877
|
.map(&:text).join(" ")
|
|
683
878
|
end
|
|
684
879
|
|
|
685
|
-
def
|
|
686
|
-
node.xpath("mods:role", NAMESPACE).
|
|
687
|
-
val = role_term_value(role)
|
|
688
|
-
return val if val
|
|
689
|
-
end
|
|
690
|
-
nil
|
|
880
|
+
def name_roles(node)
|
|
881
|
+
node.xpath("mods:role", NAMESPACE).filter_map { |role| role_term_value(role) }
|
|
691
882
|
end
|
|
692
883
|
|
|
884
|
+
# The first declared role, which is what decides whether the simple edit
|
|
885
|
+
# form owns a name. Deliberately not #name_roles.include?("Creator"):
|
|
886
|
+
# widening it would hand the form a name whose other roles it cannot
|
|
887
|
+
# represent, and saving would drop them from the preservation XML.
|
|
888
|
+
def name_role(node) = name_roles(node).first
|
|
889
|
+
|
|
693
890
|
# Prefer the type="text" roleTerm; fall back to the raw type="code" term
|
|
694
891
|
# (NOT MARC-relator-translated -- see README). nil if neither is present.
|
|
695
892
|
def role_term_value(role)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: neu-mods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|