Module: NEU::MODS::Selectors

Included in:
Document
Defined in:
lib/neu/mods/selectors.rb

Overview

Node LOCATION over a parsed MODS document. These return live Nokogiri nodes, so they serve BOTH the read path (projection reads their text) AND the write path (Cerberus's MODSMerge mutates the returned nodes in place). That shared definition is the point: the node an editor changes is provably the node the projection reads. Mixed into Document; operates on doc.

Instance Method Summary collapse

Instance Method Details

#abstract_nodesObject

All top-level elements (MODS permits several).



45
46
47
# File 'lib/neu/mods/selectors.rb', line 45

def abstract_nodes
  doc.xpath("/mods:mods/mods:abstract", NAMESPACE)
end

#build_corporate_name(name:, role: "Creator") ⇒ Object

Build a plain corporate-creator node: a single namePart + a text roleTerm. No authority/valueURI.



92
93
94
95
96
97
98
# File 'lib/neu/mods/selectors.rb', line 92

def build_corporate_name(name:, role: "Creator")
  node = build_node("name")
  node["type"] = "corporate"
  node.add_child(name_part(name)) unless name.to_s.strip.empty?
  node.add_child(role_node(role))
  node
end

#build_node(name, text = nil) ⇒ Object

Build a namespaced MODS element reusing the document's existing mods: namespace declaration (so new nodes never re-declare xmlns).



60
61
62
63
64
65
# File 'lib/neu/mods/selectors.rb', line 60

def build_node(name, text = nil)
  node = Nokogiri::XML::Node.new(name, doc)
  node.namespace = doc.root.namespace_definitions.find { |d| d.prefix == "mods" }
  node.content = text unless text.nil?
  node
end

#build_personal_name(given:, family:, role: "Creator") ⇒ Object

Build a plain personal-creator node: namePart/[family]

  • a text roleTerm. No authority/valueURI (the editable set). role is parameterised (default "Creator") so a later role-selectable form is a non-breaking change.


81
82
83
84
85
86
87
88
# File 'lib/neu/mods/selectors.rb', line 81

def build_personal_name(given:, family:, role: "Creator")
  name = build_node("name")
  name["type"] = "personal"
  name.add_child(name_part(given, "given")) unless given.to_s.strip.empty?
  name.add_child(name_part(family, "family")) unless family.to_s.strip.empty?
  name.add_child(role_node(role))
  name
end

#editable_creator_nodes(type) ⇒ Object

The "editable creator" nodes of a given @type ("personal" / "corporate") that the Advanced form manages: plain names (no authority markers) with a Creator role. The write-path counterpart to the editable_*_creators projections; everything else (authority-bearing or non-Creator) is curated and left untouched. Mirrors keyword_subjects.



72
73
74
75
# File 'lib/neu/mods/selectors.rb', line 72

def editable_creator_nodes(type)
  doc.xpath("/mods:mods/mods:name[@type='#{type}']", NAMESPACE)
     .select { |n| editable_creator_name?(n) }
end

#keyword_subjectsObject

The "keyword" subjects the simple form manages: attribute-free elements whose element children are all . Anything with an authority/valueURI (or a non-topic child, e.g. a subject) is curated and left untouched. (Distinct from the projection's #topical_subjects, which harvests every for the access copy.)



54
55
56
# File 'lib/neu/mods/selectors.rb', line 54

def keyword_subjects
  doc.xpath("/mods:mods/mods:subject", NAMESPACE).select { |s| keyword_subject?(s) }
end

#primary_title_infoObject

Top-level primary titleInfo, falling back to the first top-level titleInfo that is NOT a variant. Scoped to direct children of mods:mods so a relatedItem's nested titleInfo (e.g. a series title) is never matched.

MODS does not require usage="primary", so the fallback fires often, and an unfiltered one let document order decide the record's title. It also decided which node an edit form wrote to: MODSMerge overwrites the node this returns, so an alternative title reached here was destroyed on the next title edit, leaving the record with no primary title at all. nil is the right answer instead -- MODSMerge creates a proper primary titleInfo from nil, and each variant is projected under its own field.

Where a record carries two UNTYPED titleInfo and marks neither, .first decides and the second reaches no field. That is the schema's answer rather than a shortfall: @usage is fixed="primary" and exists precisely to nominate the principal title, @type is a closed enumeration of the four variants, and MODS 3.5 gives a legitimate second untyped title an duplicate carries none of those, so MODS gives it no meaning to preserve, and it stays in the preservation XML with no projected field.



31
32
33
34
# File 'lib/neu/mods/selectors.rb', line 31

def primary_title_info
  doc.at_xpath("/mods:mods/mods:titleInfo[@usage='primary']", NAMESPACE) ||
    doc.xpath("/mods:mods/mods:titleInfo", NAMESPACE).reject { |ti| variant_title?(ti) }.first
end

#variant_title?(node) ⇒ Boolean

MODS enumerates titleInfo/@type as exactly abbreviated, translated, alternative and uniform -- every one of them a variant. So the presence of any @type marks a variant, which also keeps an unrecognised or misspelled value out of the write path rather than guessing at it.

Returns:

  • (Boolean)


40
41
42
# File 'lib/neu/mods/selectors.rb', line 40

def variant_title?(node)
  !NEU::MODS.canonical_ws(node["type"].to_s).empty?
end