neu-mods 0.13.1 → 0.14.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/CHANGELOG.md +57 -0
- data/README.md +30 -0
- data/lib/neu/mods/projection.rb +154 -17
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07f1d513851b2423d9d5f9c5a12537aef16005ed8e86e699d34cdd3b01b65a5a
|
|
4
|
+
data.tar.gz: fa862f2409dc0dd04344179cf400bd06ad24f725178f43ce64459133c9e05cdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7867291e7502916ebdb5f409827158867f97c667b1907dfeb979a6034287daa2a1db714b11185fb5fe50e2aaafb9da9177d391f0b4241b98b6e545ef3d9c901f
|
|
7
|
+
data.tar.gz: 47fcd25686fc79ccdd1446a242c9d226277ce93cdccccaadd9b36f164087c2cfdc3f052f4fa7df2db171dc6a5ea806afbf69be59338736c4f6328f93d623ed1a
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.14.0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Consumers pin this gem, so a change to a projected SHAPE belongs here: Atlas
|
|
4
|
+
derives its access-copy attribute set and its Solr indexer from the projection,
|
|
5
|
+
and Cerberus pre-fills its edit forms from it. A shape change that reaches them
|
|
6
|
+
unannounced is one they discover as a nil value or a missing display row.
|
|
7
|
+
|
|
8
|
+
Releases before 0.14.0 are not recorded. Their diffs are in git; this file
|
|
9
|
+
starts where the convention does.
|
|
10
|
+
|
|
11
|
+
## 0.14.0
|
|
12
|
+
|
|
13
|
+
The projection gains the facts a consumer needs to offer a metadata value as a
|
|
14
|
+
browse link. Additive, but four entry shapes grow keys.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **The vocabulary a value was taken from**, as `authority:`,
|
|
19
|
+
`authority_uri:` and `value_uri:` on `names`, `origin_agents`, `languages`,
|
|
20
|
+
`genres` and `subject_headings`. Resolved off the element holding the value,
|
|
21
|
+
then off an enclosing `<subject>`; never off a `<role>`/`<roleTerm>`, which
|
|
22
|
+
names the relator's vocabulary rather than the name's. The gem tested these
|
|
23
|
+
attributes to decide editability (`Selectors#editable_creator_name?`,
|
|
24
|
+
`#keyword_subject?`) and projected none of them.
|
|
25
|
+
- **`subject_headings[:heading]`** — the parts joined with ` -- `. The join
|
|
26
|
+
moves into the gem because the composed heading is now both the string a
|
|
27
|
+
display renders and the string a browse index holds; two callers joining
|
|
28
|
+
independently is how those two drift apart. `parts:` stays, for the Advanced
|
|
29
|
+
edit form and for a consumer wanting one step.
|
|
30
|
+
- **`subject_headings[:axis]`** — the MODS element the heading's main term came
|
|
31
|
+
from, which is its first child carrying heading text. A consumer cannot
|
|
32
|
+
derive it from `parts:`, and it is what says which browse a heading belongs
|
|
33
|
+
to: `Salt marshes -- Massachusetts` is a topic heading with a place
|
|
34
|
+
subdivision, not a place. A `<name>` axis splits by `@type` into
|
|
35
|
+
`personal_name` or `corporate_name`.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **`corporate_name_subjects` now includes a `<subject><name>` with no
|
|
40
|
+
`@type`.** Such a name already displayed, because `#subject_heading_part`
|
|
41
|
+
never consulted `@type`, and it reached no axis, because both axis
|
|
42
|
+
projections required it. Corporate rather than personal: MODS expects
|
|
43
|
+
`@type="personal"` on a person, and the untyped subject names DRS holds are
|
|
44
|
+
institutional. `personal_name_subjects` is unchanged.
|
|
45
|
+
|
|
46
|
+
### Not changed
|
|
47
|
+
|
|
48
|
+
- **The per-axis subject projections stay plain string arrays.**
|
|
49
|
+
`topical_subjects` and its siblings carry no authority: `subject_headings`
|
|
50
|
+
reports it per heading, which is what a display and a browse index both read,
|
|
51
|
+
and nothing consumes a vocabulary on the flat axes.
|
|
52
|
+
- **The other labeled fields carry no authority either.** `format`, `extent`,
|
|
53
|
+
`classification` and the rest keep `{ value:, display_label:, href: }`.
|
|
54
|
+
Nothing gates on their vocabulary, and three more keys on fifteen fields is
|
|
55
|
+
JSON no consumer reads.
|
|
56
|
+
- **No new `FIELDS` entry.** Every change above is a key on an existing entry,
|
|
57
|
+
so a consumer deriving its schema from the registry needs no new column.
|
data/README.md
CHANGED
|
@@ -91,6 +91,21 @@ doc.origin_agents # => [{ name:, roles:, affiliation:, display_label:, href:,
|
|
|
91
91
|
# projects nil. The four fields that JOIN several elements into one string --
|
|
92
92
|
# abstract and the three accessCondition fields -- take companion scalars
|
|
93
93
|
# instead (doc.abstract_display_label, doc.abstract_href).
|
|
94
|
+
|
|
95
|
+
# A BROWSABLE projection also carries the vocabulary its value was taken from,
|
|
96
|
+
# as { authority:, authority_uri:, value_uri: } -- names (and originInfo
|
|
97
|
+
# agents), languages, genres and subject headings. A consumer asking "may this
|
|
98
|
+
# value be offered as a browse?" asks for any of the three: MODS lets a record
|
|
99
|
+
# declare its vocabulary by URI alone, so requiring @authority would call an
|
|
100
|
+
# authorityURI-bearing corporate name uncontrolled.
|
|
101
|
+
#
|
|
102
|
+
# This resolves DIFFERENTLY from the pair above, which is why it is a separate
|
|
103
|
+
# port rather than three more keys on qualifiers_of. A header often comes from
|
|
104
|
+
# a PARENT; an authority never does. It is read off the element holding the
|
|
105
|
+
# value, then off an enclosing <subject> -- a pre-coordinated heading declares
|
|
106
|
+
# its vocabulary once, on the heading. A <role>/<roleTerm> authority is never
|
|
107
|
+
# consulted: it is the vocabulary of the RELATOR, and the deposit form writes a
|
|
108
|
+
# marcrelator roleTerm on every creator it collects.
|
|
94
109
|
doc.host_collections
|
|
95
110
|
# => [{ title:, volume:, issue:, start_page:, end_page:,
|
|
96
111
|
# date:, text:, details: [...], extents: [...] }, ...]
|
|
@@ -111,6 +126,21 @@ doc.location # => [{ physical_location:, shelf_location:, url:,
|
|
|
111
126
|
# display_label:, href: }, ...]
|
|
112
127
|
doc.map_data # => [{ scale:, projection:, coordinates: }, ...]
|
|
113
128
|
doc.title_subjects # => ["The Great Gatsby"] composed like the main title
|
|
129
|
+
doc.subject_headings
|
|
130
|
+
# => [{ parts: ["Salt marshes", "Massachusetts"],
|
|
131
|
+
# heading: "Salt marshes -- Massachusetts",
|
|
132
|
+
# axis: "topic", authority: "lcsh", authority_uri:,
|
|
133
|
+
# value_uri:, display_label:, href: }, ...]
|
|
134
|
+
# one top-level <subject> as ONE heading. :heading is the
|
|
135
|
+
# parts joined, here rather than with the caller because
|
|
136
|
+
# it is both the string a display renders and the string
|
|
137
|
+
# a browse index holds -- two joins is how those drift.
|
|
138
|
+
# :axis names the element of the heading's MAIN term
|
|
139
|
+
# (its first child carrying heading text), which is what
|
|
140
|
+
# says which browse the heading belongs to: a place
|
|
141
|
+
# subdivision does not make a topic heading a place.
|
|
142
|
+
# A <name> axis splits by @type: "personal_name" or
|
|
143
|
+
# "corporate_name"
|
|
114
144
|
doc.hierarchical_geographic_subjects
|
|
115
145
|
# => [{ country:, state:, city:, ... }, ...] eleven levels,
|
|
116
146
|
# structured for the reason map_data is
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -164,18 +164,49 @@ module NEU
|
|
|
164
164
|
# MARC code rather than a place name.
|
|
165
165
|
HEADING_OMITTED_CHILDREN = %w[cartographics geographicCode].freeze
|
|
166
166
|
|
|
167
|
+
# What a cataloguer puts between the steps of a pre-coordinated heading.
|
|
168
|
+
# Here rather than with the consumer because the composed heading is a
|
|
169
|
+
# projected value now: a display and an index that both read it cannot
|
|
170
|
+
# separate it differently. Spaces included, which is the separator DRS
|
|
171
|
+
# has displayed for years.
|
|
172
|
+
HEADING_SEPARATOR = " -- "
|
|
173
|
+
|
|
174
|
+
# A <subject><name> with no @type reaches the corporate axis. The display
|
|
175
|
+
# already composes such a name, because #subject_heading_part never
|
|
176
|
+
# consulted @type, so the alternative is a heading a reader sees and no
|
|
177
|
+
# browse holds. Corporate rather than personal: MODS expects
|
|
178
|
+
# @type="personal" on a person, and the untyped subject names DRS holds
|
|
179
|
+
# are institutional.
|
|
180
|
+
TYPELESS_NAME_SUBJECT_TYPE = "corporate"
|
|
181
|
+
|
|
167
182
|
# Every top-level <subject> as ONE heading, its parts in document order.
|
|
168
183
|
# A pre-coordinated heading like "Salt marshes--Massachusetts--20th
|
|
169
184
|
# century" is a single statement, and the per-axis fields below cannot say
|
|
170
185
|
# which parts belonged together: they pool every topic on the record into
|
|
171
186
|
# one list, so a fragment of a heading and a whole heading read alike.
|
|
172
187
|
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
188
|
+
# The parts stay, because the Advanced edit form and a consumer wanting
|
|
189
|
+
# one step of a heading both ask for them. `heading:` is the same parts
|
|
190
|
+
# joined, and it is here rather than left to each caller because the
|
|
191
|
+
# composed heading is now BOTH the string a display renders and the
|
|
192
|
+
# string a browse index holds. Two callers joining independently is how
|
|
193
|
+
# a displayed value and an indexed value drift apart, and this join
|
|
194
|
+
# makes them the same string rather than two that happen to match.
|
|
195
|
+
#
|
|
196
|
+
# `axis:` names the MODS element the heading's MAIN term came from, which
|
|
197
|
+
# is the first child that carries heading text. A consumer cannot derive
|
|
198
|
+
# it from the parts -- they are bare strings -- and it is the fact that
|
|
199
|
+
# says which browse a heading belongs to: "Salt marshes -- Massachusetts"
|
|
200
|
+
# is a topic heading with a place subdivision, not a place. Reported as
|
|
201
|
+
# the element name, so the browse vocabulary stays with the consumer.
|
|
175
202
|
def subject_headings
|
|
176
203
|
doc.xpath("/mods:mods/mods:subject", NAMESPACE).filter_map do |node|
|
|
204
|
+
axis_node = heading_axis_node(node)
|
|
177
205
|
parts = subject_heading_parts(node)
|
|
178
|
-
|
|
206
|
+
next if axis_node.nil? || parts.empty?
|
|
207
|
+
|
|
208
|
+
{ parts: parts, heading: parts.join(HEADING_SEPARATOR), axis: heading_axis(axis_node),
|
|
209
|
+
**authority_of(axis_node), **qualifiers_of(node) }
|
|
179
210
|
end
|
|
180
211
|
end
|
|
181
212
|
|
|
@@ -246,6 +277,10 @@ module NEU
|
|
|
246
277
|
# and without it a consumer grouping role-less names can only guess.
|
|
247
278
|
usage: attr_value(node, "usage"),
|
|
248
279
|
alternative_names: alternative_names(node),
|
|
280
|
+
# The vocabulary this name was taken from, read off the <name> alone.
|
|
281
|
+
# A marcrelator <roleTerm> inside it says what the person DID, not
|
|
282
|
+
# which list the name came from -- see #authority_of.
|
|
283
|
+
**authority_of(node),
|
|
249
284
|
**qualifiers_of(node)
|
|
250
285
|
}
|
|
251
286
|
end
|
|
@@ -320,11 +355,15 @@ module NEU
|
|
|
320
355
|
# recover it from the term.
|
|
321
356
|
def languages
|
|
322
357
|
doc.xpath("/mods:mods/mods:language", NAMESPACE).filter_map do |lang|
|
|
323
|
-
|
|
358
|
+
node = language_term_node(lang)
|
|
359
|
+
term = language_term_of(node)
|
|
324
360
|
next unless term
|
|
325
361
|
|
|
362
|
+
# The authority comes off the <languageTerm> the term was read from,
|
|
363
|
+
# never off the <language> around it: MODS carries @authority on the
|
|
364
|
+
# term, and a code-only record declares `iso639-2b` there.
|
|
326
365
|
{ term: term, object_part: attr_value(lang, "objectPart"), script: script_term(lang),
|
|
327
|
-
**qualifiers_of(lang) }
|
|
366
|
+
**authority_of(node), **qualifiers_of(lang) }
|
|
328
367
|
end
|
|
329
368
|
end
|
|
330
369
|
|
|
@@ -341,7 +380,11 @@ module NEU
|
|
|
341
380
|
def extent = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:extent", from: "..")
|
|
342
381
|
def digital_origin = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:digitalOrigin", from: "..")
|
|
343
382
|
|
|
344
|
-
|
|
383
|
+
# A genre is a browse axis, so its entry carries the vocabulary the term
|
|
384
|
+
# came from. The other labeled fields do not: nothing gates on their
|
|
385
|
+
# vocabulary, and three more keys on fifteen fields is JSON no consumer
|
|
386
|
+
# reads.
|
|
387
|
+
def genres = labeled_texts_at("/mods:mods/mods:genre", authority: true)
|
|
345
388
|
|
|
346
389
|
# Who the resource is for. The last displayed top-level element with no
|
|
347
390
|
# projection at all: a record naming its audience said so to nobody.
|
|
@@ -922,6 +965,11 @@ module NEU
|
|
|
922
965
|
# display would otherwise never let a curator change.
|
|
923
966
|
def main_title_display_label = attr_value(primary_title_info, "displayLabel")
|
|
924
967
|
|
|
968
|
+
# The three attributes MODS uses to declare where a value came from, and
|
|
969
|
+
# the projected key each reports under. Read by #authority_of below.
|
|
970
|
+
AUTHORITY_ATTRIBUTES = { authority: "authority", authority_uri: "authorityURI",
|
|
971
|
+
value_uri: "valueURI" }.freeze
|
|
972
|
+
|
|
925
973
|
private
|
|
926
974
|
|
|
927
975
|
# --- helpers -------------------------------------------------------------
|
|
@@ -1072,6 +1120,48 @@ module NEU
|
|
|
1072
1120
|
{ display_label: attr_value(node, "displayLabel"), href: xlink_href(node) }
|
|
1073
1121
|
end
|
|
1074
1122
|
|
|
1123
|
+
# The attributes naming the vocabulary a VALUE was taken from, which is
|
|
1124
|
+
# what tells a controlled term apart from one a depositor typed. A
|
|
1125
|
+
# consumer gating a browse link on "is this term controlled?" asks for
|
|
1126
|
+
# any of the three: MODS lets a record declare its vocabulary by URI
|
|
1127
|
+
# alone, so requiring @authority would call an authorityURI-bearing name
|
|
1128
|
+
# uncontrolled.
|
|
1129
|
+
#
|
|
1130
|
+
# NOT part of #qualifiers_of, and the difference is the resolution rule
|
|
1131
|
+
# rather than taste. That pair answers "where does the HEADER come
|
|
1132
|
+
# from", which is often a PARENT -- six projections pass `from:` for
|
|
1133
|
+
# exactly that reason, because MODS puts @displayLabel on originInfo and
|
|
1134
|
+
# physicalDescription rather than on the publisher or extent inside
|
|
1135
|
+
# them. An authority is the opposite: `<form authority="marcform">`
|
|
1136
|
+
# carries it on the element holding the text, so reading it off the
|
|
1137
|
+
# label's element would find nothing there and attribute a parent's
|
|
1138
|
+
# vocabulary to a child elsewhere.
|
|
1139
|
+
#
|
|
1140
|
+
# Each attribute resolves on the element, then on an enclosing
|
|
1141
|
+
# <subject>: a pre-coordinated heading declares its vocabulary once, on
|
|
1142
|
+
# the heading, and every part of it belongs to that vocabulary.
|
|
1143
|
+
#
|
|
1144
|
+
# A <role>/<roleTerm> authority is never consulted, which falls out of
|
|
1145
|
+
# only ever reading the element and its <subject> ancestor. That matters
|
|
1146
|
+
# because the deposit form writes a marcrelator roleTerm on every
|
|
1147
|
+
# creator it collects, so an "any authority in the subtree" check would
|
|
1148
|
+
# call every depositor-entered name controlled.
|
|
1149
|
+
def authority_of(node)
|
|
1150
|
+
heading = enclosing_subject(node)
|
|
1151
|
+
AUTHORITY_ATTRIBUTES.transform_values do |attribute|
|
|
1152
|
+
attr_value(node, attribute) || attr_value(heading, attribute)
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# The <subject> a node sits inside, matched by namespace rather than by
|
|
1157
|
+
# prefix for the reason #xlink_href is: a document binds the MODS
|
|
1158
|
+
# namespace to whatever prefix it likes.
|
|
1159
|
+
def enclosing_subject(node)
|
|
1160
|
+
node&.ancestors&.find do |ancestor|
|
|
1161
|
+
ancestor.name == "subject" && ancestor.namespace&.href == NAMESPACE["mods"]
|
|
1162
|
+
end
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1075
1165
|
# xlink:href by namespace rather than by prefix. A document is free to
|
|
1076
1166
|
# bind the XLink namespace to any prefix, or to none, and node["xlink:href"]
|
|
1077
1167
|
# matches the literal prefix alone.
|
|
@@ -1086,8 +1176,9 @@ module NEU
|
|
|
1086
1176
|
# header from. That is not always the element holding the text: MODS puts
|
|
1087
1177
|
# @displayLabel on originInfo and physicalDescription, never on the
|
|
1088
1178
|
# publisher, place, extent or digitalOrigin inside them.
|
|
1089
|
-
def labeled(value, label_node)
|
|
1090
|
-
{ value: value, **qualifiers_of(label_node) }
|
|
1179
|
+
def labeled(value, label_node, authority_node: nil)
|
|
1180
|
+
entry = { value: value, **qualifiers_of(label_node) }
|
|
1181
|
+
authority_node ? entry.merge(authority_of(authority_node)) : entry
|
|
1091
1182
|
end
|
|
1092
1183
|
|
|
1093
1184
|
# The qualifiers of an originInfo block. @eventType says what the block
|
|
@@ -1125,10 +1216,16 @@ module NEU
|
|
|
1125
1216
|
# #texts_at, with each value carrying the qualifiers of its element.
|
|
1126
1217
|
# `from:` is an XPath relative to the text-bearing node, naming the
|
|
1127
1218
|
# ancestor the header comes from instead.
|
|
1128
|
-
|
|
1219
|
+
# `authority:` adds the vocabulary of the VALUE, which resolves off the
|
|
1220
|
+
# text-bearing node even where `from:` points the header at an ancestor
|
|
1221
|
+
# -- `<form authority="marcform">` is exactly that shape.
|
|
1222
|
+
def labeled_texts_at(xpath, from: nil, authority: false)
|
|
1129
1223
|
doc.xpath(xpath, NAMESPACE).filter_map do |node|
|
|
1130
1224
|
value = clean(node.text)
|
|
1131
|
-
|
|
1225
|
+
next unless value
|
|
1226
|
+
|
|
1227
|
+
labeled(value, from ? node.at_xpath(from, NAMESPACE) : node,
|
|
1228
|
+
authority_node: authority ? node : nil)
|
|
1132
1229
|
end
|
|
1133
1230
|
end
|
|
1134
1231
|
|
|
@@ -1184,11 +1281,33 @@ module NEU
|
|
|
1184
1281
|
end
|
|
1185
1282
|
end
|
|
1186
1283
|
|
|
1284
|
+
# The corporate axis also takes a subject name with NO @type (see
|
|
1285
|
+
# TYPELESS_NAME_SUBJECT_TYPE), so a name the heading composes reaches a
|
|
1286
|
+
# browse instead of displaying and projecting nowhere.
|
|
1187
1287
|
def name_subjects(type)
|
|
1188
|
-
|
|
1288
|
+
predicate = "@type='#{type}'"
|
|
1289
|
+
predicate = "#{predicate} or not(@type)" if type == TYPELESS_NAME_SUBJECT_TYPE
|
|
1290
|
+
doc.xpath("/mods:mods/mods:subject/mods:name[#{predicate}]", NAMESPACE)
|
|
1189
1291
|
.filter_map { |node| name_display_value_w_date(node) }
|
|
1190
1292
|
end
|
|
1191
1293
|
|
|
1294
|
+
# The child a heading's axis and authority come from: the first one
|
|
1295
|
+
# carrying heading text. A heading with none projects nothing, so its
|
|
1296
|
+
# axis is never asked for.
|
|
1297
|
+
def heading_axis_node(node)
|
|
1298
|
+
node.xpath("mods:*", NAMESPACE).find { |child| subject_heading_part(child).compact.any? }
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# The axis of one heading child, as the MODS element that holds it. A
|
|
1302
|
+
# <name> splits by @type, because a person and an organisation are
|
|
1303
|
+
# separate browses and MODS says which on the element rather than in the
|
|
1304
|
+
# element name.
|
|
1305
|
+
def heading_axis(child)
|
|
1306
|
+
return "#{attr_value(child, "type") || TYPELESS_NAME_SUBJECT_TYPE}_name" if child.name == "name"
|
|
1307
|
+
|
|
1308
|
+
snake_case(child.name)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1192
1311
|
def subject_heading_parts(node)
|
|
1193
1312
|
node.xpath("mods:*", NAMESPACE).flat_map { |child| subject_heading_part(child) }.compact
|
|
1194
1313
|
end
|
|
@@ -1302,6 +1421,13 @@ module NEU
|
|
|
1302
1421
|
[head, *rest.map(&:capitalize)].join
|
|
1303
1422
|
end
|
|
1304
1423
|
|
|
1424
|
+
# #camelize's inverse, for reporting a schema element name as a projected
|
|
1425
|
+
# one: "hierarchicalGeographic" -> "hierarchical_geographic". Hand-rolled
|
|
1426
|
+
# for the reason the gem has no Rails dependency at all.
|
|
1427
|
+
def snake_case(name)
|
|
1428
|
+
name.to_s.gsub(/([a-z])([A-Z])/) { "#{Regexp.last_match(1)}_#{Regexp.last_match(2).downcase}" }
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1305
1431
|
def texts_under(node, xpath)
|
|
1306
1432
|
node.xpath(xpath, NAMESPACE).filter_map { |child| clean(child.text) }
|
|
1307
1433
|
end
|
|
@@ -1310,13 +1436,24 @@ module NEU
|
|
|
1310
1436
|
doc.xpath(xpath, NAMESPACE).filter_map { |node| clean(node.text) }
|
|
1311
1437
|
end
|
|
1312
1438
|
|
|
1313
|
-
#
|
|
1314
|
-
#
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1439
|
+
# Which <languageTerm> the language of one element is read from, text
|
|
1440
|
+
# form preferred. The node is located separately from its text because
|
|
1441
|
+
# the authority governing the term sits on that element: the value and
|
|
1442
|
+
# its vocabulary have to come from one node rather than from two
|
|
1443
|
+
# independent lookups.
|
|
1444
|
+
def language_term_node(lang)
|
|
1445
|
+
lang.at_xpath("mods:languageTerm[@type='text']", NAMESPACE) ||
|
|
1446
|
+
lang.at_xpath("mods:languageTerm", NAMESPACE)
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
# A text term as the record wrote it; a code through the ISO 639
|
|
1450
|
+
# registry, so a record saying `eng` projects "English".
|
|
1451
|
+
def language_term_of(node)
|
|
1452
|
+
return nil unless node
|
|
1453
|
+
|
|
1454
|
+
return clean(node.text) if attr_value(node, "type") == "text"
|
|
1318
1455
|
|
|
1319
|
-
code = clean(
|
|
1456
|
+
code = clean(node.text)
|
|
1320
1457
|
code && LanguageCodes.term(code)
|
|
1321
1458
|
end
|
|
1322
1459
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: neu-mods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
@@ -63,6 +63,7 @@ extensions: []
|
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
65
|
- ".version"
|
|
66
|
+
- CHANGELOG.md
|
|
66
67
|
- Gemfile
|
|
67
68
|
- README.md
|
|
68
69
|
- Rakefile
|