Module: NEU::MODS::Projection
- Included in:
- Document
- Defined in:
- lib/neu/mods/projection.rb
Overview
Node -> plain data. The read contract: what a MODS document projects to for
indexing/display. Behavior-preserving with Atlas's prior mods-gem-based
extraction (verified by the conformance corpus), reimplemented in Nokogiri so
DRS depends on Nokogiri alone. Mixed into Document; operates on doc.
Empty-value conventions mirror Atlas: scalar fields are "" when absent
(matching .text.squish on an empty node set), except permanent_url and
date_created, which are nil when their node is absent. Arrays are [].
Constant Summary collapse
- NON_SORT_BINDING =
Characters that bind a nonSort to the word after it. An elided article takes no space -- "L'Etranger", not "L' Etranger" -- and the same holds for a hyphenated prefix. U+2019 is the curly apostrophe, escaped rather than literal to keep lib/ pure ASCII (see the source-purity spec).
["'", "\u2019", "-"].freeze
Class Method Summary collapse
-
.compose_title(parts) ⇒ Object
Pure title composition over a parts hash, factored out of #plain_title so callers that already hold the parts -- e.g.
-
.join_non_sort(non_sort, title) ⇒ Object
MODS says a nonSort carries whatever separator it needs, so the historical composition simply concatenated.
Instance Method Summary collapse
-
#abstract ⇒ Object
--- Abstract / access ---------------------------------------------------.
- #access_condition ⇒ Object
-
#date_created ⇒ Object
Parsed dateCreated, or nil if no originInfo/dateCreated, or "" if present but unparseable (mirrors Atlas's safe_date_parse rescue).
- #digital_origin ⇒ Object
- #editable_corporate_creators ⇒ Object
-
#editable_personal_creators ⇒ Object
Editable (depositor-managed) creators: the plain names (no authority markers) with a Creator role, as STRUCTURED parts for form pre-fill -- distinct from #names, which composes display strings for the access copy.
- #extent ⇒ Object
- #format ⇒ Object
- #genres ⇒ Object
- #identifiers ⇒ Object
-
#keywords ⇒ Object
The editable free-text keyword set (Cerberus simple form): topics under the attribute-free keyword subjects only.
-
#languages ⇒ Object
--- Scalars / simple arrays --------------------------------------------.
-
#names ⇒ Object
All top-level names as { name:, role: }.
- #permanent_url ⇒ Object
-
#plain_title ⇒ Object
Composed display title (the former Atlas MODSDecoration#plain_title), driven off the scoped primary title.
-
#preserved_names ⇒ Object
Names the editable form does NOT manage (authority-bearing or non-Creator) -- for read-only display ("these exist; edit via the XML tab").
- #related_series ⇒ Object
- #resource_type ⇒ Object
-
#title_parts ⇒ Object
Structured primary-title parts.
-
#to_h ⇒ Object
The complete read projection, keyed to Atlas's Metadata::MODS attribute names -- a drop-in source for
convert_xml_to_json. -
#topical_subjects ⇒ Object
Every
under any top-level (the access-copy projection, equivalent to Atlas's extract_topical_subjects).
Class Method Details
.compose_title(parts) ⇒ Object
Pure title composition over a parts hash, factored out of #plain_title so callers that already hold the parts -- e.g. Atlas's access-copy model -- can compose the display title WITHOUT re-parsing XML on the read path (reaching for Nokogiri in a decorator is the smell this avoids). Keys: :non_sort :title :subtitle :part_name :part_number (nil or "" for absent). Returns "" when there is no title. Exposed as NEU::MODS.compose_title.
43 44 45 46 47 48 49 |
# File 'lib/neu/mods/projection.rb', line 43 def self.compose_title(parts) return "" if parts[:title].to_s.strip.empty? optional = { ": " => parts[:subtitle], " - " => parts[:part_name], ", " => parts[:part_number] } suffix = optional.filter_map { |sep, val| "#{sep}#{val}" unless val.to_s.strip.empty? }.join "#{join_non_sort(parts[:non_sort], parts[:title])}#{suffix}" end |
.join_non_sort(non_sort, title) ⇒ Object
MODS says a nonSort carries whatever separator it needs, so the historical
composition simply concatenated. That only holds while the authored
trailing space survives, and it does not: #child_text canonicalizes
whitespace on read, so <nonSort>The </nonSort> arrives here as "The" and
the title came out as "TheHobbit". Composing the separator instead makes
the output right whether or not the source kept one -- which matters,
because an invisible trailing space is not something a curator, a
hand-edit or a third-party producer can be relied on to preserve.
Callers that DO pass the space (Atlas's access-copy model) are unaffected: a nonSort already ending in whitespace is joined as-is.
68 69 70 71 72 73 74 |
# File 'lib/neu/mods/projection.rb', line 68 def self.join_non_sort(non_sort, title) prefix = non_sort.to_s return title.to_s if prefix.empty? return "#{prefix}#{title}" if prefix.end_with?(" ") || prefix.end_with?(*NON_SORT_BINDING) "#{prefix} #{title}" end |
Instance Method Details
#abstract ⇒ Object
--- Abstract / access ---------------------------------------------------
78 79 80 |
# File 'lib/neu/mods/projection.rb', line 78 def abstract join_paragraphs(abstract_nodes) end |
#access_condition ⇒ Object
82 83 84 |
# File 'lib/neu/mods/projection.rb', line 82 def access_condition join_paragraphs(doc.xpath("/mods:mods/mods:accessCondition", NAMESPACE)) end |
#date_created ⇒ Object
Parsed dateCreated, or nil if no originInfo/dateCreated, or "" if present but unparseable (mirrors Atlas's safe_date_parse rescue).
169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/neu/mods/projection.rb', line 169 def date_created node = doc.at_xpath("/mods:mods/mods:originInfo/mods:dateCreated", NAMESPACE) return nil unless node str = NEU::MODS.canonical_ws(node.text) return nil if str.empty? begin DateTime.parse(str) rescue Date::Error "" end end |
#digital_origin ⇒ Object
147 |
# File 'lib/neu/mods/projection.rb', line 147 def digital_origin = text_at("/mods:mods/mods:physicalDescription/mods:digitalOrigin") |
#editable_corporate_creators ⇒ Object
121 122 123 |
# File 'lib/neu/mods/projection.rb', line 121 def editable_corporate_creators editable_creator_nodes("corporate").map { |node| { name: clean_part(non_date_parts_joined(node)) } } end |
#editable_personal_creators ⇒ Object
Editable (depositor-managed) creators: the plain names (no authority markers) with a Creator role, as STRUCTURED parts for form pre-fill -- distinct from #names, which composes display strings for the access copy.
115 116 117 118 119 |
# File 'lib/neu/mods/projection.rb', line 115 def editable_personal_creators editable_creator_nodes("personal").map do |node| { given: clean_part(joined_parts(node, "given")), family: clean_part(joined_parts(node, "family")) } end end |
#extent ⇒ Object
146 |
# File 'lib/neu/mods/projection.rb', line 146 def extent = text_at("/mods:mods/mods:physicalDescription/mods:extent") |
#format ⇒ Object
145 |
# File 'lib/neu/mods/projection.rb', line 145 def format = text_at("/mods:mods/mods:physicalDescription/mods:form") |
#genres ⇒ Object
149 150 151 |
# File 'lib/neu/mods/projection.rb', line 149 def genres doc.xpath("/mods:mods/mods:genre", NAMESPACE).map { |g| clean(g.text) } end |
#identifiers ⇒ Object
158 159 160 |
# File 'lib/neu/mods/projection.rb', line 158 def identifiers doc.xpath("/mods:mods/mods:identifier", NAMESPACE).map { |i| clean(i.text) } end |
#keywords ⇒ Object
The editable free-text keyword set (Cerberus simple form): topics under the attribute-free keyword subjects only.
90 91 92 |
# File 'lib/neu/mods/projection.rb', line 90 def keywords keyword_subjects.flat_map { |s| s.xpath("mods:topic", NAMESPACE).map { |t| t.text.strip } } end |
#languages ⇒ Object
--- Scalars / simple arrays --------------------------------------------
136 137 138 139 140 141 142 |
# File 'lib/neu/mods/projection.rb', line 136 def languages doc.xpath("/mods:mods/mods:language", NAMESPACE).map do |lang| term = lang.at_xpath("mods:languageTerm[@type='text']", NAMESPACE) || lang.at_xpath("mods:languageTerm", NAMESPACE) clean(term&.text) end.compact end |
#names ⇒ Object
All top-level names as { name:, role: }. name reproduces the mods gem's
display_value_w_date (including its quirks -- faithfully, so existing Solr/
display output is preserved). role prefers the type="text" roleTerm,
falling back to the raw code (NOT MARC-relator-translated -- see README).
106 107 108 109 110 |
# File 'lib/neu/mods/projection.rb', line 106 def names doc.xpath("/mods:mods/mods:name", NAMESPACE).map do |node| { name: name_display_value_w_date(node), role: name_role(node) } end end |
#permanent_url ⇒ Object
162 163 164 165 |
# File 'lib/neu/mods/projection.rb', line 162 def permanent_url node = doc.at_xpath("/mods:mods/mods:identifier[@type='hdl']", NAMESPACE) node && clean(node.text) end |
#plain_title ⇒ Object
Composed display title (the former Atlas MODSDecoration#plain_title), driven off the scoped primary title.
33 34 35 |
# File 'lib/neu/mods/projection.rb', line 33 def plain_title Projection.compose_title(title_parts) end |
#preserved_names ⇒ Object
Names the editable form does NOT manage (authority-bearing or non-Creator) -- for read-only display ("these exist; edit via the XML tab"). Composed display string + role, like #names but filtered to the preserved set.
128 129 130 131 132 |
# File 'lib/neu/mods/projection.rb', line 128 def preserved_names doc.xpath("/mods:mods/mods:name", NAMESPACE) .reject { |node| editable_creator_name?(node) } .map { |node| { name: name_display_value_w_date(node), role: name_role(node) } } end |
#related_series ⇒ Object
153 154 155 156 |
# File 'lib/neu/mods/projection.rb', line 153 def doc.xpath("/mods:mods/mods:relatedItem[@type='series']/mods:titleInfo/mods:title", NAMESPACE) .map { |t| clean(t.text) } end |
#resource_type ⇒ Object
144 |
# File 'lib/neu/mods/projection.rb', line 144 def resource_type = text_at("/mods:mods/mods:typeOfResource") |
#title_parts ⇒ Object
Structured primary-title parts. nil for an absent part (the Cerberus form treats nil as "not present"); to_h coerces to "" for the Atlas main_title.
20 21 22 23 24 25 26 27 28 29 |
# File 'lib/neu/mods/projection.rb', line 20 def title_parts ti = primary_title_info { non_sort: child_text(ti, "mods:nonSort"), subtitle: child_text(ti, "mods:subTitle"), title: child_text(ti, "mods:title"), part_name: child_text(ti, "mods:partName"), part_number: child_text(ti, "mods:partNumber") } end |
#to_h ⇒ Object
The complete read projection, keyed to Atlas's Metadata::MODS attribute
names -- a drop-in source for convert_xml_to_json.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/neu/mods/projection.rb', line 187 def to_h { main_title: title_parts.transform_values(&:to_s), names: names, languages: languages, date_created: date_created, resource_type: resource_type, genres: genres, format: format, extent: extent, digital_origin: digital_origin, abstract: abstract, related_series: , topical_subjects: topical_subjects, identifiers: identifiers, permanent_url: permanent_url, access_condition: access_condition } end |
#topical_subjects ⇒ Object
Every
96 97 98 |
# File 'lib/neu/mods/projection.rb', line 96 def topical_subjects doc.xpath("/mods:mods/mods:subject/mods:topic", NAMESPACE).map { |t| clean(t.text) } end |