neu-mods 0.1.1 → 0.3.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 +15 -0
- data/lib/neu/mods/projection.rb +54 -1
- data/lib/neu/mods/selectors.rb +55 -0
- metadata +12 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebf2bf82cd0b9c0e5593307ecc9a986cefcb16f4035a5004f233bdd7f9d4d8ea
|
|
4
|
+
data.tar.gz: 7087e35df1b2f34430a53acfa49e9c97be12dd9b8bdc36b3ffa69d05e98189e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63b83e54cdf64af06856fe1de2cb3763cc36a1f1f0a73403fb7836fac9e910dcd2f0ebd0a4fd5fb928b7714ddec20ad281ac1cc42b47a18fcda21cbc8ad8589f
|
|
7
|
+
data.tar.gz: 3d6ecb5f48c1c4cff669add4adc7bfa2a90bf29045b17bb1b1047a58f02820c85fbe303ec0c0e7665f7045e27d03531fe32759f6bc023d91032038baf4466c65
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
data/README.md
CHANGED
|
@@ -44,8 +44,23 @@ NEU::MODS.compose_title(non_sort: "", title: "What's New",
|
|
|
44
44
|
node = doc.primary_title_info.at_xpath("mods:title", NEU::MODS::NAMESPACE)
|
|
45
45
|
node.content = "New Title" unless NEU::MODS.whitespace_equivalent?(node.text, "New Title")
|
|
46
46
|
doc.to_xml
|
|
47
|
+
|
|
48
|
+
# Editable creators (for an "advanced metadata" form): structured read,
|
|
49
|
+
# node selection (for replace-on-save), and structure-aware build.
|
|
50
|
+
doc.editable_personal_creators # => [{ given:, family: }] (plain, Creator role)
|
|
51
|
+
doc.editable_corporate_creators # => [{ name: }]
|
|
52
|
+
doc.preserved_names # => [{ name:, role: }] (authority-bearing / non-Creator — read-only)
|
|
53
|
+
doc.editable_creator_nodes("personal") # => live <name> nodes to replace
|
|
54
|
+
doc.build_personal_name(given: "Jenny", family: "Smith") # => a plain personal <name> node
|
|
55
|
+
doc.build_corporate_name(name: "Northeastern University") # => a plain corporate <name> node
|
|
47
56
|
```
|
|
48
57
|
|
|
58
|
+
The "editable creator" set is plain names — **no `@authority`/`@authorityURI`/
|
|
59
|
+
`@valueURI`** — with a **Creator** role; everything else (authority-controlled or
|
|
60
|
+
other-role names) is `preserved_names`, shown read-only. This mirrors the
|
|
61
|
+
keyword-subject curated-vs-editable split. `build_*_name`'s `role:` defaults to
|
|
62
|
+
`"Creator"` but is parameterised, so a later role-selectable form is non-breaking.
|
|
63
|
+
|
|
49
64
|
## Two normalizers, two jobs
|
|
50
65
|
|
|
51
66
|
- `NEU::MODS.whitespace_equivalent?` / `.canonical_ws` — the **no-op guard**: did an
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -45,7 +45,32 @@ module NEU
|
|
|
45
45
|
|
|
46
46
|
optional = { ": " => parts[:subtitle], " - " => parts[:part_name], ", " => parts[:part_number] }
|
|
47
47
|
suffix = optional.filter_map { |sep, val| "#{sep}#{val}" unless val.to_s.strip.empty? }.join
|
|
48
|
-
"#{parts[:non_sort]
|
|
48
|
+
"#{join_non_sort(parts[:non_sort], parts[:title])}#{suffix}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Characters that bind a nonSort to the word after it. An elided article
|
|
52
|
+
# takes no space -- "L'Etranger", not "L' Etranger" -- and the same holds
|
|
53
|
+
# for a hyphenated prefix. U+2019 is the curly apostrophe, escaped rather
|
|
54
|
+
# than literal to keep lib/ pure ASCII (see the source-purity spec).
|
|
55
|
+
NON_SORT_BINDING = ["'", "\u2019", "-"].freeze
|
|
56
|
+
|
|
57
|
+
# MODS says a nonSort carries whatever separator it needs, so the historical
|
|
58
|
+
# composition simply concatenated. That only holds while the authored
|
|
59
|
+
# trailing space survives, and it does not: #child_text canonicalizes
|
|
60
|
+
# whitespace on read, so `<nonSort>The </nonSort>` arrives here as "The" and
|
|
61
|
+
# the title came out as "TheHobbit". Composing the separator instead makes
|
|
62
|
+
# the output right whether or not the source kept one -- which matters,
|
|
63
|
+
# because an invisible trailing space is not something a curator, a
|
|
64
|
+
# hand-edit or a third-party producer can be relied on to preserve.
|
|
65
|
+
#
|
|
66
|
+
# Callers that DO pass the space (Atlas's access-copy model) are unaffected:
|
|
67
|
+
# a nonSort already ending in whitespace is joined as-is.
|
|
68
|
+
def self.join_non_sort(non_sort, title)
|
|
69
|
+
prefix = non_sort.to_s
|
|
70
|
+
return title.to_s if prefix.empty?
|
|
71
|
+
return "#{prefix}#{title}" if prefix.end_with?(" ") || prefix.end_with?(*NON_SORT_BINDING)
|
|
72
|
+
|
|
73
|
+
"#{prefix} #{title}"
|
|
49
74
|
end
|
|
50
75
|
|
|
51
76
|
# --- Abstract / access ---------------------------------------------------
|
|
@@ -84,6 +109,28 @@ module NEU
|
|
|
84
109
|
end
|
|
85
110
|
end
|
|
86
111
|
|
|
112
|
+
# Editable (depositor-managed) creators: the plain names (no authority
|
|
113
|
+
# markers) with a Creator role, as STRUCTURED parts for form pre-fill --
|
|
114
|
+
# distinct from #names, which composes display strings for the access copy.
|
|
115
|
+
def editable_personal_creators
|
|
116
|
+
editable_creator_nodes("personal").map do |node|
|
|
117
|
+
{ given: clean_part(joined_parts(node, "given")), family: clean_part(joined_parts(node, "family")) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def editable_corporate_creators
|
|
122
|
+
editable_creator_nodes("corporate").map { |node| { name: clean_part(non_date_parts_joined(node)) } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Names the editable form does NOT manage (authority-bearing or non-Creator)
|
|
126
|
+
# -- for read-only display ("these exist; edit via the XML tab"). Composed
|
|
127
|
+
# display string + role, like #names but filtered to the preserved set.
|
|
128
|
+
def preserved_names
|
|
129
|
+
doc.xpath("/mods:mods/mods:name", NAMESPACE)
|
|
130
|
+
.reject { |node| editable_creator_name?(node) }
|
|
131
|
+
.map { |node| { name: name_display_value_w_date(node), role: name_role(node) } }
|
|
132
|
+
end
|
|
133
|
+
|
|
87
134
|
# --- Scalars / simple arrays --------------------------------------------
|
|
88
135
|
|
|
89
136
|
def languages
|
|
@@ -184,6 +231,12 @@ module NEU
|
|
|
184
231
|
v.empty? ? nil : v
|
|
185
232
|
end
|
|
186
233
|
|
|
234
|
+
# canonical_ws keeping "" for blank -- for structured form-field values
|
|
235
|
+
# (an empty given/family/org renders as an empty input, not a dropped key).
|
|
236
|
+
def clean_part(str)
|
|
237
|
+
NEU::MODS.canonical_ws(str)
|
|
238
|
+
end
|
|
239
|
+
|
|
187
240
|
def join_paragraphs(nodes)
|
|
188
241
|
nodes.map { |n| NEU::MODS.normalize_paragraphs(n.text) }.reject(&:empty?).join("\n\n")
|
|
189
242
|
end
|
data/lib/neu/mods/selectors.rb
CHANGED
|
@@ -39,6 +39,39 @@ module NEU
|
|
|
39
39
|
node
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# The "editable creator" <name> nodes of a given @type ("personal" /
|
|
43
|
+
# "corporate") that the Advanced form manages: plain names (no authority
|
|
44
|
+
# markers) with a Creator role. The write-path counterpart to the
|
|
45
|
+
# editable_*_creators projections; everything else (authority-bearing or
|
|
46
|
+
# non-Creator) is curated and left untouched. Mirrors keyword_subjects.
|
|
47
|
+
def editable_creator_nodes(type)
|
|
48
|
+
doc.xpath("/mods:mods/mods:name[@type='#{type}']", NAMESPACE)
|
|
49
|
+
.select { |n| editable_creator_name?(n) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build a plain personal-creator <name> node: namePart[@type=given]/[family]
|
|
53
|
+
# + a text roleTerm. No authority/valueURI (the editable set). `role` is
|
|
54
|
+
# parameterised (default "Creator") so a later role-selectable form is a
|
|
55
|
+
# non-breaking change.
|
|
56
|
+
def build_personal_name(given:, family:, role: "Creator")
|
|
57
|
+
name = build_node("name")
|
|
58
|
+
name["type"] = "personal"
|
|
59
|
+
name.add_child(name_part(given, "given")) unless given.to_s.strip.empty?
|
|
60
|
+
name.add_child(name_part(family, "family")) unless family.to_s.strip.empty?
|
|
61
|
+
name.add_child(role_node(role))
|
|
62
|
+
name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build a plain corporate-creator <name> node: a single namePart + a text
|
|
66
|
+
# roleTerm. No authority/valueURI.
|
|
67
|
+
def build_corporate_name(name:, role: "Creator")
|
|
68
|
+
node = build_node("name")
|
|
69
|
+
node["type"] = "corporate"
|
|
70
|
+
node.add_child(name_part(name)) unless name.to_s.strip.empty?
|
|
71
|
+
node.add_child(role_node(role))
|
|
72
|
+
node
|
|
73
|
+
end
|
|
74
|
+
|
|
42
75
|
private
|
|
43
76
|
|
|
44
77
|
def keyword_subject?(subject)
|
|
@@ -47,6 +80,28 @@ module NEU
|
|
|
47
80
|
topics = subject.element_children
|
|
48
81
|
topics.any? && topics.all? { |c| c.name == "topic" }
|
|
49
82
|
end
|
|
83
|
+
|
|
84
|
+
# A name is "editable" (depositor-managed) when it carries no authority
|
|
85
|
+
# markers and resolves to a Creator role. Shared by editable_creator_nodes
|
|
86
|
+
# (write/select) and the editable_*_creators projections (read).
|
|
87
|
+
def editable_creator_name?(node)
|
|
88
|
+
%w[authority authorityURI valueURI].none? { |attr| node[attr] } &&
|
|
89
|
+
name_role(node) == "Creator"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def name_part(text, type = nil)
|
|
93
|
+
np = build_node("namePart", text.to_s.strip)
|
|
94
|
+
np["type"] = type if type
|
|
95
|
+
np
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def role_node(role)
|
|
99
|
+
role_el = build_node("role")
|
|
100
|
+
term = build_node("roleTerm", role)
|
|
101
|
+
term["type"] = "text"
|
|
102
|
+
role_el.add_child(term)
|
|
103
|
+
role_el
|
|
104
|
+
end
|
|
50
105
|
end
|
|
51
106
|
end
|
|
52
107
|
end
|
metadata
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: neu-mods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- -
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '1.13'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- -
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.13'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: rspec
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - ~>
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
33
|
version: '3.12'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - ~>
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '3.12'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rubocop
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - ~>
|
|
45
|
+
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '1.60'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - ~>
|
|
52
|
+
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '1.60'
|
|
55
55
|
description: 'Nokogiri-native, dependency-light reading/projection contract over MODS
|
|
@@ -62,7 +62,7 @@ executables: []
|
|
|
62
62
|
extensions: []
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
|
-
- .version
|
|
65
|
+
- ".version"
|
|
66
66
|
- Gemfile
|
|
67
67
|
- README.md
|
|
68
68
|
- Rakefile
|
|
@@ -83,16 +83,16 @@ require_paths:
|
|
|
83
83
|
- lib
|
|
84
84
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
|
-
- -
|
|
86
|
+
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '3.0'
|
|
89
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
90
|
requirements:
|
|
91
|
-
- -
|
|
91
|
+
- - ">="
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
93
|
version: '0'
|
|
94
94
|
requirements: []
|
|
95
|
-
rubygems_version: 3.
|
|
95
|
+
rubygems_version: 3.4.10
|
|
96
96
|
signing_key:
|
|
97
97
|
specification_version: 4
|
|
98
98
|
summary: Northeastern-flavored MODS XML projection + selection for the DRS.
|