neu-mods 0.1.0 → 0.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/.version +1 -1
- data/README.md +21 -0
- data/lib/neu/mods/projection.rb +40 -12
- data/lib/neu/mods/selectors.rb +55 -0
- data/lib/neu-mods.rb +5 -0
- metadata +11 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb4c30744c189d3e0b4bfc0f84bb75175685bcb142dd6dc35dc55cfbcbdaf0a2
|
|
4
|
+
data.tar.gz: a5cd73f764edf2da97e05787788124bed168c9dba8caf997307fc53591f05661
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 785e3c7485886be0fd7946b61463e2e2b66319cf4c4f30980b6e4b2b0b8ed2ae4348be41a48caa9fbad536901a89e9c73d4d4fd005283bc2cc0dc9c9102d6ff6
|
|
7
|
+
data.tar.gz: 7a331ed220702dbab23bd6d16637f33a3da2f431a17939647ee7d1fd21898b9cf76a4594b26300eb01f8c9ad1b50d0cabcd298cdbff9c363a01a7f7bba6229a7
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.2.0
|
data/README.md
CHANGED
|
@@ -34,12 +34,33 @@ doc.topical_subjects # => ["Civil society", ...] (every <topic>, for the acces
|
|
|
34
34
|
doc.keywords # => [...] (only the editable attribute-free keyword subjects)
|
|
35
35
|
doc.to_h # => full projection, keyed to Atlas's Metadata::MODS attributes
|
|
36
36
|
|
|
37
|
+
# Pure title composition (no document needed) — for callers that already hold
|
|
38
|
+
# the parts (e.g. Atlas's access-copy model) and must not re-parse XML on read.
|
|
39
|
+
NEU::MODS.compose_title(non_sort: "", title: "What's New",
|
|
40
|
+
part_name: "How We Respond to Disaster", part_number: "Episode 1")
|
|
41
|
+
# => "What's New - How We Respond to Disaster, Episode 1" (== doc.plain_title)
|
|
42
|
+
|
|
37
43
|
# Selectors (live nodes — for editing)
|
|
38
44
|
node = doc.primary_title_info.at_xpath("mods:title", NEU::MODS::NAMESPACE)
|
|
39
45
|
node.content = "New Title" unless NEU::MODS.whitespace_equivalent?(node.text, "New Title")
|
|
40
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
|
|
41
56
|
```
|
|
42
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
|
+
|
|
43
64
|
## Two normalizers, two jobs
|
|
44
65
|
|
|
45
66
|
- `NEU::MODS.whitespace_equivalent?` / `.canonical_ws` — the **no-op guard**: did an
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -31,13 +31,21 @@ module NEU
|
|
|
31
31
|
# Composed display title (the former Atlas MODSDecoration#plain_title), driven
|
|
32
32
|
# off the scoped primary title.
|
|
33
33
|
def plain_title
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
Projection.compose_title(title_parts)
|
|
35
|
+
end
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
# Pure title composition over a parts hash, factored out of #plain_title so
|
|
38
|
+
# callers that already hold the parts -- e.g. Atlas's access-copy model --
|
|
39
|
+
# can compose the display title WITHOUT re-parsing XML on the read path
|
|
40
|
+
# (reaching for Nokogiri in a decorator is the smell this avoids). Keys:
|
|
41
|
+
# :non_sort :title :subtitle :part_name :part_number (nil or "" for absent).
|
|
42
|
+
# Returns "" when there is no title. Exposed as NEU::MODS.compose_title.
|
|
43
|
+
def self.compose_title(parts)
|
|
44
|
+
return "" if parts[:title].to_s.strip.empty?
|
|
45
|
+
|
|
46
|
+
optional = { ": " => parts[:subtitle], " - " => parts[:part_name], ", " => parts[:part_number] }
|
|
47
|
+
suffix = optional.filter_map { |sep, val| "#{sep}#{val}" unless val.to_s.strip.empty? }.join
|
|
48
|
+
"#{parts[:non_sort]}#{parts[:title]}#{suffix}"
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
# --- Abstract / access ---------------------------------------------------
|
|
@@ -76,6 +84,28 @@ module NEU
|
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
|
|
87
|
+
# Editable (depositor-managed) creators: the plain names (no authority
|
|
88
|
+
# markers) with a Creator role, as STRUCTURED parts for form pre-fill --
|
|
89
|
+
# distinct from #names, which composes display strings for the access copy.
|
|
90
|
+
def editable_personal_creators
|
|
91
|
+
editable_creator_nodes("personal").map do |node|
|
|
92
|
+
{ given: clean_part(joined_parts(node, "given")), family: clean_part(joined_parts(node, "family")) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def editable_corporate_creators
|
|
97
|
+
editable_creator_nodes("corporate").map { |node| { name: clean_part(non_date_parts_joined(node)) } }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Names the editable form does NOT manage (authority-bearing or non-Creator)
|
|
101
|
+
# -- for read-only display ("these exist; edit via the XML tab"). Composed
|
|
102
|
+
# display string + role, like #names but filtered to the preserved set.
|
|
103
|
+
def preserved_names
|
|
104
|
+
doc.xpath("/mods:mods/mods:name", NAMESPACE)
|
|
105
|
+
.reject { |node| editable_creator_name?(node) }
|
|
106
|
+
.map { |node| { name: name_display_value_w_date(node), role: name_role(node) } }
|
|
107
|
+
end
|
|
108
|
+
|
|
79
109
|
# --- Scalars / simple arrays --------------------------------------------
|
|
80
110
|
|
|
81
111
|
def languages
|
|
@@ -176,12 +206,10 @@ module NEU
|
|
|
176
206
|
v.empty? ? nil : v
|
|
177
207
|
end
|
|
178
208
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def prefix(sep, val)
|
|
184
|
-
blank?(val) ? "" : "#{sep}#{val}"
|
|
209
|
+
# canonical_ws keeping "" for blank -- for structured form-field values
|
|
210
|
+
# (an empty given/family/org renders as an empty input, not a dropped key).
|
|
211
|
+
def clean_part(str)
|
|
212
|
+
NEU::MODS.canonical_ws(str)
|
|
185
213
|
end
|
|
186
214
|
|
|
187
215
|
def join_paragraphs(nodes)
|
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
|
data/lib/neu-mods.rb
CHANGED
|
@@ -33,5 +33,10 @@ module NEU
|
|
|
33
33
|
# Curator-freetext normalization for the access copy (see TextNormalizer).
|
|
34
34
|
def normalize(str) = TextNormalizer.normalize(str)
|
|
35
35
|
def normalize_paragraphs(str) = TextNormalizer.normalize_paragraphs(str)
|
|
36
|
+
|
|
37
|
+
# Pure display-title composition over a primary-title parts hash (see
|
|
38
|
+
# Projection.compose_title). Lets a caller that already holds the parts --
|
|
39
|
+
# e.g. Atlas's access-copy model -- compose the title without parsing XML.
|
|
40
|
+
def compose_title(parts) = Projection.compose_title(parts)
|
|
36
41
|
end
|
|
37
42
|
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
@@ -14,42 +14,42 @@ dependencies:
|
|
|
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.
|