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 [].
Which fields are scalar and which are arrays is declared in FIELDS, not
left to each method to decide.
Constant Summary collapse
- TITLE_SEPARATORS =
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.
nonSort, title, subtitle, partName, partNumber -- the order the librarians chose. titleInfo is an unordered choice in the schema, so no document order is available to follow and the composer has to fix one.
A period separates the title or subtitle from the parts, and one part from the next. The separator travels with its part rather than with the position, so a record giving only a partNumber still gets the period. Nothing is appended after the last part: a title is a value, not a sentence, and a trailing period reads as part of the title everywhere the value is re-used.
[[": ", :subtitle], [". ", :part_name], [". ", :part_number]].freeze
- 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
- HEADING_OMITTED_CHILDREN =
Neither child carries heading text: cartographics is a structured coordinate a reader reaches through #map_data, and geographicCode is a MARC code rather than a place name.
%w[cartographics geographicCode].freeze
- HEADING_SEPARATOR =
What a cataloguer puts between the steps of a pre-coordinated heading. Here rather than with the consumer because the composed heading is a projected value now: a display and an index that both read it cannot separate it differently. Spaces included, which is the separator DRS has displayed for years.
" -- "- TYPELESS_NAME_SUBJECT_TYPE =
A
with no @type reaches the corporate axis. The display already composes such a name, because #subject_heading_part never consulted @type, so the alternative is a heading a reader sees and no browse holds. Corporate rather than personal: MODS expects @type="personal" on a person, and the untyped subject names DRS holds are institutional. "corporate"- MARC_COUNTRY_AUTHORITY =
Prefer the type="text" term per place, falling back to a coded one -- the pattern #role_term_value and #languages already use.
A bare marccountry code is the exception, and it drops. "mau" is not a place name, and unfiltered it reached the display and the Solr places facet as one, sitting in the list beside Boston. That is the call #geographic_code_subjects already makes for a MARC GAC code. A code under any other authority survives, because there the code may be the only statement the record made and nothing here can say it is not text.
TODO: expand a marccountry code through a registry, as LanguageCodes does for eng -> English. That needs a vendored code list, and would let this project "Massachusetts" instead of dropping the element.
"marccountry"- NAMED_RELATED_ITEM_TYPES =
relatedItem @type values that already have a field of their own, so the catch-all below does not repeat them.
%w[series host].freeze
- NAMED_HOST_DETAIL_TYPES =
The part detail types volume and issue already have named keys on a host entry, so #host_details does not repeat them -- the same split NAMED_RELATED_ITEM_TYPES makes for relatedItem. A caption is kept because it is the label a cataloguer wrote for the number ("chap." before "3"), which no consumer can reconstruct from an open @type.
%w[volume issue].freeze
- HIERARCHICAL_GEOGRAPHIC_LEVELS =
The eleven children the XSD allows under hierarchicalGeographic, in the order MODS lists them -- broadest first, which is also the order a consumer composing a place string wants to reverse.
%i[ continent country province region state territory county city city_section island area ].freeze
- RECORD_INFO_PARTS =
recordInfo children. Read as a single value: the schema repeats the element, but a record with two cataloguing provenances is not a case anyone has, and an array here buys nothing.
{ content_source: "mods:recordContentSource", origin: "mods:recordOrigin", description_standard: "mods:descriptionStandard", creation_date: "mods:recordCreationDate", change_date: "mods:recordChangeDate", language_of_cataloging: "mods:languageOfCataloging/mods:languageTerm" }.freeze
- W3CDTF_DATE =
The w3cdtf date shapes a date element may stop at: year, year-month, a full date, or a full date with a time. Matching the shape explicitly, rather than widening DateTime.parse, is what lets the declared precision fall out of the parse instead of being guessed after it.
A value outside these shapes is NOT a date, and #parse_w3cdtf says so rather than guessing. Ruby's DateTime.parse fills the components it cannot find from the CURRENT date, so "19uu" -- standard MARC 008 fill, which the v1 corpus carries at scale -- asserted today's date at "day" precision, and the assertion changed daily. What the record actually wrote survives in the matching *_text field instead.
/\A(\d{4})(?:-(\d{2})(?:-(\d{2})(T\S+)?)?)?\z/- ISO8601_BASIC_DATE =
ISO 8601 basic format: the same year, month and day written without the hyphens. Accepted ONLY where the record declares @encoding="iso8601", because eight bare digits are a date only because the encoding says so -- an accession number is eight digits too, and guessing is the mistake dropping the DateTime.parse fallback exists to prevent.
/\A(\d{4})(?:(\d{2})(?:(\d{2})(T\S+)?)?)?\z/- ISO8601_ENCODING =
@encoding, folded. MODS leaves the attribute an open string and records write "iso8601" and "ISO-8601" alike.
"iso8601"- EMPTY_DATE =
What #date_parts returns when the element is absent entirely, so an absent date is distinguishable from one present and unparseable.
{ value: nil, precision: nil, end_value: nil, end_precision: nil, qualifier: nil, key_date: nil, text: nil, display_label: nil, event_type: nil }.freeze
- DATE_ELEMENTS =
Everything a record declared about one originInfo date, as { value:, precision:, end_value:, end_precision:, qualifier:, key_date:, text: }.
A date is not a scalar. Precision established that: a year-only date parses to January 1st, and no consumer downstream can tell that month and day from a record that claimed them. A range and a qualifier are the same kind of claim, and dropping them breaks the same rule -- a preservation repository must not project a value the record did not give. A ranged record was worse than that: #at_xpath took the first node, so one end of the range was PROMOTED to be the date, and the output was indistinguishable from a single certain year.
The parts are projected as separate flat fields rather than one nested value, because the value half has three consumers that need a real date object -- a Solr sort key, a citation year and an OAI date. Those three are also why the literal gets its own field rather than sharing the value: a sort key cannot hold "ca. 1920", and a display can. MODS puts seven date elements under originInfo and this reads all of them. dateCaptured is when the object was digitised and dateModified is when the resource changed -- preservation and cataloguing provenance, which a consumer may keep off a page but cannot recover from anywhere else. dateValid is the period the content holds for, and dateOther is where a date fitting no other element lands, which is where a quantity of migrated v1 date data goes. The seven date elements MODS puts under originInfo, in the order a consumer deciding a place header reads them.
%w[dateIssued dateCreated copyrightDate dateCaptured dateValid dateOther dateModified].freeze
- FIELDS =
The field registry: the single declaration of what this gem projects. Field name => cardinality, :one or :many. The projection method of the same name owns the XPath; this row says the field exists and whether it is single- or multi-valued. #to_h is derived from it, and Atlas derives its Metadata::MODS attr_json set from it, so a field cannot be projected here and go undeclared there (or the reverse).
Cardinality is the half that earns its keep. The at_xpath-versus-xpath choice here and the single-versus-array column choice in Atlas used to be made independently in two repos with nothing tying them together, which is how repeatable MODS elements ended up truncated to their first match. #cardinality_of checks each method against its row.
{ # titles main_title: :one, main_title_display_label: :one, alternative_title: :many, uniform_title: :many, translated_title: :many, abbreviated_title: :many, names: :many, languages: :many, abstract: :one, abstract_display_label: :one, abstract_href: :one, # origin publication_information: :many, place_of_publication: :many, origin_agents: :many, edition: :many, issuance: :many, frequency: :many, # Seven rows per originInfo date, for each of the seven MODS defines. # Flat rather than one nested value, because the value half has # consumers that need a real date object. date_created: :one, date_created_precision: :one, date_created_end: :one, date_created_end_precision: :one, date_created_qualifier: :one, date_created_key_date: :one, date_created_text: :one, date_created_display_label: :one, date_created_event_type: :one, date_issued: :one, date_issued_precision: :one, date_issued_end: :one, date_issued_end_precision: :one, date_issued_qualifier: :one, date_issued_key_date: :one, date_issued_text: :one, date_issued_display_label: :one, date_issued_event_type: :one, copyright_date: :one, copyright_date_precision: :one, copyright_date_end: :one, copyright_date_end_precision: :one, copyright_date_qualifier: :one, copyright_date_key_date: :one, copyright_date_text: :one, copyright_date_display_label: :one, copyright_date_event_type: :one, date_captured: :one, date_captured_precision: :one, date_captured_end: :one, date_captured_end_precision: :one, date_captured_qualifier: :one, date_captured_key_date: :one, date_captured_text: :one, date_captured_display_label: :one, date_captured_event_type: :one, date_valid: :one, date_valid_precision: :one, date_valid_end: :one, date_valid_end_precision: :one, date_valid_qualifier: :one, date_valid_key_date: :one, date_valid_text: :one, date_valid_display_label: :one, date_valid_event_type: :one, date_other: :one, date_other_precision: :one, date_other_end: :one, date_other_end_precision: :one, date_other_qualifier: :one, date_other_key_date: :one, date_other_text: :one, date_other_display_label: :one, date_other_event_type: :one, date_modified: :one, date_modified_precision: :one, date_modified_end: :one, date_modified_end_precision: :one, date_modified_qualifier: :one, date_modified_key_date: :one, date_modified_text: :one, date_modified_display_label: :one, date_modified_event_type: :one, # physical description resource_type: :many, target_audience: :many, genres: :many, format: :many, extent: :many, digital_origin: :many, reformatting_quality: :many, physical_description_notes: :many, notes: :many, table_of_contents: :many, # subjects subject_headings: :many, topical_subjects: :many, geographic_subjects: :many, temporal_subjects: :many, personal_name_subjects: :many, corporate_name_subjects: :many, occupation_subjects: :many, genre_subjects: :many, geographic_code_subjects: :many, title_subjects: :many, hierarchical_geographic_subjects: :many, map_data: :many, # related items related_series: :many, host_collections: :many, related_items: :many, # identifiers and location identifiers: :many, classification: :many, permanent_url: :one, permanent_url_display_label: :one, record_info: :one, location: :many, # access access_condition: :one, access_condition_display_label: :one, access_condition_href: :one, use_and_reproduction: :one, use_and_reproduction_display_label: :one, use_and_reproduction_href: :one, restriction_on_access: :one, restriction_on_access_display_label: :one, restriction_on_access_href: :one }.freeze
- AUTHORITY_ATTRIBUTES =
The three attributes MODS uses to declare where a value came from, and the projected key each reports under. Read by #authority_of below.
{ authority: "authority", authority_uri: "authorityURI", value_uri: "valueURI" }.freeze
Class Method Summary collapse
-
.cardinality_of(value) ⇒ Object
The cardinality a projected value actually has, for checking a value against its FIELDS row.
- .compose_title(parts) ⇒ Object
-
.fold_type(str) ⇒ Object
An open-string @type reduced to its letters and digits, so casing, word separators and camelCasing cannot decide whether a field matches.
-
.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
- #abbreviated_title ⇒ Object
-
#abstract ⇒ Object
--- Abstract / access ---------------------------------------------------.
-
#abstract_display_label ⇒ Object
The header and the link a record attached to its abstract.
- #abstract_href ⇒ Object
-
#access_condition ⇒ Object
Every top-level accessCondition joined, regardless of @type.
-
#access_condition_display_label ⇒ Object
Companion scalars for the same reason the abstract's are: each of the three fields joins several elements into one value, and a licence URI belongs beside the licence text a reader is given.
- #access_condition_href ⇒ Object
-
#access_title_parts ⇒ Object
The title parts as the access copy wants them: normalised like the abstract, so a curly quote, an invisible format mark or a Windows-1252 control cannot reach Solr or a display template.
-
#alternative_names(node) ⇒ Object
mods:alternativeName, new in MODS 3.7: a second form of the same name, not a second name.
-
#alternative_title ⇒ Object
The variant titles, each composed and normalised like the main title.
-
#classification ⇒ Object
An LCC or DDC call number.
- #copyright_date ⇒ Object
- #copyright_date_display_label ⇒ Object
- #copyright_date_end ⇒ Object
- #copyright_date_end_precision ⇒ Object
- #copyright_date_event_type ⇒ Object
- #copyright_date_key_date ⇒ Object
- #copyright_date_parts ⇒ Object
- #copyright_date_precision ⇒ Object
- #copyright_date_qualifier ⇒ Object
- #copyright_date_text ⇒ Object
- #copyright_date_with_precision ⇒ Object
- #corporate_name_subjects ⇒ Object
- #date_captured ⇒ Object
- #date_captured_display_label ⇒ Object
- #date_captured_end ⇒ Object
- #date_captured_end_precision ⇒ Object
- #date_captured_event_type ⇒ Object
- #date_captured_key_date ⇒ Object
- #date_captured_parts ⇒ Object
- #date_captured_precision ⇒ Object
- #date_captured_qualifier ⇒ Object
- #date_captured_text ⇒ Object
- #date_created ⇒ Object
- #date_created_display_label ⇒ Object
- #date_created_end ⇒ Object
- #date_created_end_precision ⇒ Object
- #date_created_event_type ⇒ Object
- #date_created_key_date ⇒ Object
- #date_created_parts ⇒ Object
- #date_created_precision ⇒ Object
- #date_created_qualifier ⇒ Object
- #date_created_text ⇒ Object
-
#date_created_with_precision ⇒ Object
The [value, precision] pair the precision work introduced.
- #date_issued ⇒ Object
- #date_issued_display_label ⇒ Object
- #date_issued_end ⇒ Object
- #date_issued_end_precision ⇒ Object
- #date_issued_event_type ⇒ Object
- #date_issued_key_date ⇒ Object
- #date_issued_parts ⇒ Object
- #date_issued_precision ⇒ Object
- #date_issued_qualifier ⇒ Object
- #date_issued_text ⇒ Object
- #date_issued_with_precision ⇒ Object
- #date_modified ⇒ Object
- #date_modified_display_label ⇒ Object
- #date_modified_end ⇒ Object
- #date_modified_end_precision ⇒ Object
- #date_modified_event_type ⇒ Object
- #date_modified_key_date ⇒ Object
- #date_modified_parts ⇒ Object
- #date_modified_precision ⇒ Object
- #date_modified_qualifier ⇒ Object
- #date_modified_text ⇒ Object
- #date_other ⇒ Object
- #date_other_display_label ⇒ Object
- #date_other_end ⇒ Object
- #date_other_end_precision ⇒ Object
- #date_other_event_type ⇒ Object
- #date_other_key_date ⇒ Object
- #date_other_parts ⇒ Object
- #date_other_precision ⇒ Object
- #date_other_qualifier ⇒ Object
- #date_other_text ⇒ Object
- #date_valid ⇒ Object
- #date_valid_display_label ⇒ Object
- #date_valid_end ⇒ Object
- #date_valid_end_precision ⇒ Object
- #date_valid_event_type ⇒ Object
- #date_valid_key_date ⇒ Object
- #date_valid_parts ⇒ Object
- #date_valid_precision ⇒ Object
- #date_valid_qualifier ⇒ Object
- #date_valid_text ⇒ Object
- #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.
- #edition ⇒ Object
- #extent ⇒ Object
-
#format ⇒ Object
MODS puts @displayLabel on physicalDescription, not on the form, extent, digitalOrigin, reformattingQuality or note inside it -- so these four take the label off their parent.
-
#frequency ⇒ Object
Serials.
- #genre_subjects ⇒ Object
-
#genres ⇒ Object
A genre is a browse axis, so its entry carries the vocabulary the term came from.
-
#geographic_code_subjects ⇒ Object
A MARC GAC code.
-
#geographic_subjects ⇒ Object
The other subject axes.
-
#hierarchical_geographic_subjects ⇒ Object
Kept structured for the reason #map_data is.
-
#host_collections ⇒ Object
The host's title plus THIS work's position within it.
-
#identifiers ⇒ Object
{ type:, value: }, because a DOI, an accession number and a collection id are not the same kind of thing and no consumer can tell them apart from the digits alone -- a reader shown a bare 10.1234/x cannot see it is a DOI, and a display cannot decide to linkify it.
- #issuance ⇒ Object
-
#keywords ⇒ Object
The editable free-text keyword set (Cerberus simple form): topics under the attribute-free keyword subjects only.
-
#languages ⇒ Object
{ term:, object_part:, script: } per language element.
-
#location ⇒ Object
location repeats, and one location mixes kinds: a shelf mark and a URL are not interchangeable, and a consumer has to know which it holds before it can decide to linkify it.
-
#main_title ⇒ Object
Atlas names this field main_title; the registry requires a method per field name, and #access_title_parts is the descriptive name for what it returns.
-
#main_title_display_label ⇒ Object
What the record wants the title row headed, which is almost never set -- but a record that does set it means it, and "Title" is the one header a display would otherwise never let a curator change.
-
#map_data ⇒ Object
subject/cartographics, kept structured.
-
#name_entry(node) ⇒ Object
One name as the access copy wants it.
-
#names ⇒ Object
All top-level names as { name:, roles: }.
-
#notes ⇒ Object
Every top-level note, keeping its @type.
-
#occupation_subjects ⇒ Object
The last unprojected member of a closed set: every other subject child already has a field, so leaving this one out made "what a subject can carry" arbitrary rather than complete.
-
#origin_agents ⇒ Object
originInfo/agent, new in MODS 3.8: who performed the event the block records.
- #permanent_url ⇒ Object
-
#permanent_url_display_label ⇒ Object
The handle identifier carries @displayLabel="Permanent URL" in Atlas's own MODS template, so the header a reader sees is one the record states rather than one a decorator invents.
-
#personal_name_subjects ⇒ Object
Name subjects compose through the same display-value port as #names, so one person reads the same whether they authored the work or are its subject.
-
#physical_description_notes ⇒ Object
A note about the object rather than about the work -- "Scanned at 600 dpi" belongs beside the extent, not beside a content note.
- #place_of_publication ⇒ Object
- #place_term_value(place) ⇒ 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").
-
#publication_information ⇒ Object
originInfo repeats, and so do publisher and edition within one.
-
#record_info ⇒ Object
Who catalogued this record, to what standard, and when.
- #reformatting_quality ⇒ Object
-
#related_items ⇒ Object
Every other relatedItem, keeping its @type.
- #related_series ⇒ Object
-
#resource_type ⇒ Object
MODS repeats typeOfResource, and repeats physicalDescription (and form and extent within one), so all four are :many.
- #restriction_on_access ⇒ Object
- #restriction_on_access_display_label ⇒ Object
- #restriction_on_access_href ⇒ Object
-
#subject_headings ⇒ Object
Every top-level
as ONE heading, its parts in document order. -
#table_of_contents ⇒ Object
Read with its line breaks intact.
-
#target_audience ⇒ Object
Who the resource is for.
- #temporal_subjects ⇒ Object
-
#title_parts ⇒ Object
Structured primary-title parts, byte-faithful to the document.
-
#title_subjects ⇒ Object
A subject that is a work has a nonSort, a subTitle and part numbers like any other titleInfo, so it composes through the same port as the main title rather than taking titleInfo/title alone.
-
#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). - #translated_title ⇒ Object
- #uniform_title ⇒ Object
-
#use_and_reproduction ⇒ Object
The two @type values MODS defines, projected apart.
- #use_and_reproduction_display_label ⇒ Object
- #use_and_reproduction_href ⇒ Object
Class Method Details
.cardinality_of(value) ⇒ Object
The cardinality a projected value actually has, for checking a value against its FIELDS row. An Array is :many and anything else is :one, so a field declared :many that forgot to switch at_xpath for xpath is caught.
948 |
# File 'lib/neu/mods/projection.rb', line 948 def self.cardinality_of(value) = value.is_a?(Array) ? :many : :one |
.compose_title(parts) ⇒ Object
68 69 70 71 72 73 74 75 |
# File 'lib/neu/mods/projection.rb', line 68 def self.compose_title(parts) return "" if parts[:title].to_s.strip.empty? suffix = TITLE_SEPARATORS.filter_map do |separator, key| "#{separator}#{parts[key]}" unless parts[key].to_s.strip.empty? end.join "#{join_non_sort(parts[:non_sort], parts[:title])}#{suffix}" end |
.fold_type(str) ⇒ Object
An open-string @type reduced to its letters and digits, so casing, word separators and camelCasing cannot decide whether a field matches.
150 151 152 |
# File 'lib/neu/mods/projection.rb', line 150 def self.fold_type(str) NEU::MODS.canonical_ws(str).downcase.gsub(/[^a-z0-9]/, "") 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.
94 95 96 97 98 99 100 |
# File 'lib/neu/mods/projection.rb', line 94 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
#abbreviated_title ⇒ Object
41 |
# File 'lib/neu/mods/projection.rb', line 41 def abbreviated_title = variant_titles("abbreviated") |
#abstract ⇒ Object
--- Abstract / access ---------------------------------------------------
104 105 106 |
# File 'lib/neu/mods/projection.rb', line 104 def abstract join_paragraphs(abstract_nodes) end |
#abstract_display_label ⇒ Object
The header and the link a record attached to its abstract. Companion scalars rather than an entry, because #abstract joins every abstract element into one value and three consumers -- the OAI dc:description, the citation and description_tsim -- hold that value as a string.
112 |
# File 'lib/neu/mods/projection.rb', line 112 def abstract_display_label = first_attr(abstract_nodes, "displayLabel") |
#abstract_href ⇒ Object
113 |
# File 'lib/neu/mods/projection.rb', line 113 def abstract_href = first_href(abstract_nodes) |
#access_condition ⇒ Object
Every top-level accessCondition joined, regardless of @type. Retained because it is the only projection that carries an untyped or unrecognised accessCondition, which the two typed fields below cannot see -- a consumer that renders only those needs this as its fallback.
119 120 121 |
# File 'lib/neu/mods/projection.rb', line 119 def access_condition join_paragraphs(doc.xpath("/mods:mods/mods:accessCondition", NAMESPACE)) end |
#access_condition_display_label ⇒ Object
Companion scalars for the same reason the abstract's are: each of the three fields joins several elements into one value, and a licence URI belongs beside the licence text a reader is given.
133 |
# File 'lib/neu/mods/projection.rb', line 133 def access_condition_display_label = first_attr(access_condition_nodes, "displayLabel") |
#access_condition_href ⇒ Object
134 |
# File 'lib/neu/mods/projection.rb', line 134 def access_condition_href = first_href(access_condition_nodes) |
#access_title_parts ⇒ Object
The title parts as the access copy wants them: normalised like the abstract, so a curly quote, an invisible format mark or a Windows-1252 control cannot reach Solr or a display template. Titles and prose share one vocabulary -- the asymmetry where only prose was cleaned was the bug.
954 955 956 |
# File 'lib/neu/mods/projection.rb', line 954 def access_title_parts title_parts.transform_values { |value| NEU::MODS.normalize(value.to_s) } end |
#alternative_names(node) ⇒ Object
mods:alternativeName, new in MODS 3.7: a second form of the same name, not a second name. Composed with the ENCLOSING name's @type, because alternativeName carries @altType rather than @type and an alternative for a personal name is still a personal name -- read from its own attributes it would compose "Doe Jane" where the name above it composes "Doe, Jane".
294 295 296 297 298 |
# File 'lib/neu/mods/projection.rb', line 294 def alternative_names(node) node.xpath("mods:alternativeName", NAMESPACE).filter_map do |alt| name_display_value_w_date(alt, attr_value(node, "type")) end end |
#alternative_title ⇒ Object
The variant titles, each composed and normalised like the main title. MODS repeats titleInfo, so a record may carry more than one of a type. These are what keeps the primary-title fallback's refusal to promote a variant from hiding anything: the variant still reaches a reader, under a label that says which kind of title it is.
38 |
# File 'lib/neu/mods/projection.rb', line 38 def alternative_title = variant_titles("alternative") |
#classification ⇒ Object
An LCC or DDC call number. Note this is NOT the same concept as Atlas's classification_ssim, which carries a FileSet content-type vocabulary -- the name collision is accidental and the consumer has to pick a free Solr field.
484 |
# File 'lib/neu/mods/projection.rb', line 484 def classification = labeled_texts_at("/mods:mods/mods:classification") |
#copyright_date ⇒ Object
715 |
# File 'lib/neu/mods/projection.rb', line 715 def copyright_date = copyright_date_parts[:value] |
#copyright_date_display_label ⇒ Object
722 |
# File 'lib/neu/mods/projection.rb', line 722 def copyright_date_display_label = copyright_date_parts[:display_label] |
#copyright_date_end ⇒ Object
717 |
# File 'lib/neu/mods/projection.rb', line 717 def copyright_date_end = copyright_date_parts[:end_value] |
#copyright_date_end_precision ⇒ Object
718 |
# File 'lib/neu/mods/projection.rb', line 718 def copyright_date_end_precision = copyright_date_parts[:end_precision] |
#copyright_date_event_type ⇒ Object
723 |
# File 'lib/neu/mods/projection.rb', line 723 def copyright_date_event_type = copyright_date_parts[:event_type] |
#copyright_date_key_date ⇒ Object
720 |
# File 'lib/neu/mods/projection.rb', line 720 def copyright_date_key_date = copyright_date_parts[:key_date] |
#copyright_date_parts ⇒ Object
689 |
# File 'lib/neu/mods/projection.rb', line 689 def copyright_date_parts = date_parts("copyrightDate") |
#copyright_date_precision ⇒ Object
716 |
# File 'lib/neu/mods/projection.rb', line 716 def copyright_date_precision = copyright_date_parts[:precision] |
#copyright_date_qualifier ⇒ Object
719 |
# File 'lib/neu/mods/projection.rb', line 719 def copyright_date_qualifier = copyright_date_parts[:qualifier] |
#copyright_date_text ⇒ Object
721 |
# File 'lib/neu/mods/projection.rb', line 721 def copyright_date_text = copyright_date_parts[:text] |
#copyright_date_with_precision ⇒ Object
770 |
# File 'lib/neu/mods/projection.rb', line 770 def copyright_date_with_precision = [copyright_date, copyright_date_precision] |
#corporate_name_subjects ⇒ Object
227 |
# File 'lib/neu/mods/projection.rb', line 227 def corporate_name_subjects = name_subjects("corporate") |
#date_captured ⇒ Object
725 |
# File 'lib/neu/mods/projection.rb', line 725 def date_captured = date_captured_parts[:value] |
#date_captured_display_label ⇒ Object
732 |
# File 'lib/neu/mods/projection.rb', line 732 def date_captured_display_label = date_captured_parts[:display_label] |
#date_captured_end ⇒ Object
727 |
# File 'lib/neu/mods/projection.rb', line 727 def date_captured_end = date_captured_parts[:end_value] |
#date_captured_end_precision ⇒ Object
728 |
# File 'lib/neu/mods/projection.rb', line 728 def date_captured_end_precision = date_captured_parts[:end_precision] |
#date_captured_event_type ⇒ Object
733 |
# File 'lib/neu/mods/projection.rb', line 733 def date_captured_event_type = date_captured_parts[:event_type] |
#date_captured_key_date ⇒ Object
730 |
# File 'lib/neu/mods/projection.rb', line 730 def date_captured_key_date = date_captured_parts[:key_date] |
#date_captured_parts ⇒ Object
690 |
# File 'lib/neu/mods/projection.rb', line 690 def date_captured_parts = date_parts("dateCaptured") |
#date_captured_precision ⇒ Object
726 |
# File 'lib/neu/mods/projection.rb', line 726 def date_captured_precision = date_captured_parts[:precision] |
#date_captured_qualifier ⇒ Object
729 |
# File 'lib/neu/mods/projection.rb', line 729 def date_captured_qualifier = date_captured_parts[:qualifier] |
#date_captured_text ⇒ Object
731 |
# File 'lib/neu/mods/projection.rb', line 731 def date_captured_text = date_captured_parts[:text] |
#date_created ⇒ Object
695 |
# File 'lib/neu/mods/projection.rb', line 695 def date_created = date_created_parts[:value] |
#date_created_display_label ⇒ Object
702 |
# File 'lib/neu/mods/projection.rb', line 702 def date_created_display_label = date_created_parts[:display_label] |
#date_created_end ⇒ Object
697 |
# File 'lib/neu/mods/projection.rb', line 697 def date_created_end = date_created_parts[:end_value] |
#date_created_end_precision ⇒ Object
698 |
# File 'lib/neu/mods/projection.rb', line 698 def date_created_end_precision = date_created_parts[:end_precision] |
#date_created_event_type ⇒ Object
703 |
# File 'lib/neu/mods/projection.rb', line 703 def date_created_event_type = date_created_parts[:event_type] |
#date_created_key_date ⇒ Object
700 |
# File 'lib/neu/mods/projection.rb', line 700 def date_created_key_date = date_created_parts[:key_date] |
#date_created_parts ⇒ Object
687 |
# File 'lib/neu/mods/projection.rb', line 687 def date_created_parts = date_parts("dateCreated") |
#date_created_precision ⇒ Object
696 |
# File 'lib/neu/mods/projection.rb', line 696 def date_created_precision = date_created_parts[:precision] |
#date_created_qualifier ⇒ Object
699 |
# File 'lib/neu/mods/projection.rb', line 699 def date_created_qualifier = date_created_parts[:qualifier] |
#date_created_text ⇒ Object
701 |
# File 'lib/neu/mods/projection.rb', line 701 def date_created_text = date_created_parts[:text] |
#date_created_with_precision ⇒ Object
The [value, precision] pair the precision work introduced. Retained because it is the documented entry point for a caller that wants both halves and nothing else.
768 |
# File 'lib/neu/mods/projection.rb', line 768 def date_created_with_precision = [date_created, date_created_precision] |
#date_issued ⇒ Object
705 |
# File 'lib/neu/mods/projection.rb', line 705 def date_issued = date_issued_parts[:value] |
#date_issued_display_label ⇒ Object
712 |
# File 'lib/neu/mods/projection.rb', line 712 def date_issued_display_label = date_issued_parts[:display_label] |
#date_issued_end ⇒ Object
707 |
# File 'lib/neu/mods/projection.rb', line 707 def date_issued_end = date_issued_parts[:end_value] |
#date_issued_end_precision ⇒ Object
708 |
# File 'lib/neu/mods/projection.rb', line 708 def date_issued_end_precision = date_issued_parts[:end_precision] |
#date_issued_event_type ⇒ Object
713 |
# File 'lib/neu/mods/projection.rb', line 713 def date_issued_event_type = date_issued_parts[:event_type] |
#date_issued_key_date ⇒ Object
710 |
# File 'lib/neu/mods/projection.rb', line 710 def date_issued_key_date = date_issued_parts[:key_date] |
#date_issued_parts ⇒ Object
688 |
# File 'lib/neu/mods/projection.rb', line 688 def date_issued_parts = date_parts("dateIssued") |
#date_issued_precision ⇒ Object
706 |
# File 'lib/neu/mods/projection.rb', line 706 def date_issued_precision = date_issued_parts[:precision] |
#date_issued_qualifier ⇒ Object
709 |
# File 'lib/neu/mods/projection.rb', line 709 def date_issued_qualifier = date_issued_parts[:qualifier] |
#date_issued_text ⇒ Object
711 |
# File 'lib/neu/mods/projection.rb', line 711 def date_issued_text = date_issued_parts[:text] |
#date_issued_with_precision ⇒ Object
769 |
# File 'lib/neu/mods/projection.rb', line 769 def date_issued_with_precision = [date_issued, date_issued_precision] |
#date_modified ⇒ Object
755 |
# File 'lib/neu/mods/projection.rb', line 755 def date_modified = date_modified_parts[:value] |
#date_modified_display_label ⇒ Object
762 |
# File 'lib/neu/mods/projection.rb', line 762 def date_modified_display_label = date_modified_parts[:display_label] |
#date_modified_end ⇒ Object
757 |
# File 'lib/neu/mods/projection.rb', line 757 def date_modified_end = date_modified_parts[:end_value] |
#date_modified_end_precision ⇒ Object
758 |
# File 'lib/neu/mods/projection.rb', line 758 def date_modified_end_precision = date_modified_parts[:end_precision] |
#date_modified_event_type ⇒ Object
763 |
# File 'lib/neu/mods/projection.rb', line 763 def date_modified_event_type = date_modified_parts[:event_type] |
#date_modified_key_date ⇒ Object
760 |
# File 'lib/neu/mods/projection.rb', line 760 def date_modified_key_date = date_modified_parts[:key_date] |
#date_modified_parts ⇒ Object
693 |
# File 'lib/neu/mods/projection.rb', line 693 def date_modified_parts = date_parts("dateModified") |
#date_modified_precision ⇒ Object
756 |
# File 'lib/neu/mods/projection.rb', line 756 def date_modified_precision = date_modified_parts[:precision] |
#date_modified_qualifier ⇒ Object
759 |
# File 'lib/neu/mods/projection.rb', line 759 def date_modified_qualifier = date_modified_parts[:qualifier] |
#date_modified_text ⇒ Object
761 |
# File 'lib/neu/mods/projection.rb', line 761 def date_modified_text = date_modified_parts[:text] |
#date_other ⇒ Object
745 |
# File 'lib/neu/mods/projection.rb', line 745 def date_other = date_other_parts[:value] |
#date_other_display_label ⇒ Object
752 |
# File 'lib/neu/mods/projection.rb', line 752 def date_other_display_label = date_other_parts[:display_label] |
#date_other_end ⇒ Object
747 |
# File 'lib/neu/mods/projection.rb', line 747 def date_other_end = date_other_parts[:end_value] |
#date_other_end_precision ⇒ Object
748 |
# File 'lib/neu/mods/projection.rb', line 748 def date_other_end_precision = date_other_parts[:end_precision] |
#date_other_event_type ⇒ Object
753 |
# File 'lib/neu/mods/projection.rb', line 753 def date_other_event_type = date_other_parts[:event_type] |
#date_other_key_date ⇒ Object
750 |
# File 'lib/neu/mods/projection.rb', line 750 def date_other_key_date = date_other_parts[:key_date] |
#date_other_parts ⇒ Object
692 |
# File 'lib/neu/mods/projection.rb', line 692 def date_other_parts = date_parts("dateOther") |
#date_other_precision ⇒ Object
746 |
# File 'lib/neu/mods/projection.rb', line 746 def date_other_precision = date_other_parts[:precision] |
#date_other_qualifier ⇒ Object
749 |
# File 'lib/neu/mods/projection.rb', line 749 def date_other_qualifier = date_other_parts[:qualifier] |
#date_other_text ⇒ Object
751 |
# File 'lib/neu/mods/projection.rb', line 751 def date_other_text = date_other_parts[:text] |
#date_valid ⇒ Object
735 |
# File 'lib/neu/mods/projection.rb', line 735 def date_valid = date_valid_parts[:value] |
#date_valid_display_label ⇒ Object
742 |
# File 'lib/neu/mods/projection.rb', line 742 def date_valid_display_label = date_valid_parts[:display_label] |
#date_valid_end ⇒ Object
737 |
# File 'lib/neu/mods/projection.rb', line 737 def date_valid_end = date_valid_parts[:end_value] |
#date_valid_end_precision ⇒ Object
738 |
# File 'lib/neu/mods/projection.rb', line 738 def date_valid_end_precision = date_valid_parts[:end_precision] |
#date_valid_event_type ⇒ Object
743 |
# File 'lib/neu/mods/projection.rb', line 743 def date_valid_event_type = date_valid_parts[:event_type] |
#date_valid_key_date ⇒ Object
740 |
# File 'lib/neu/mods/projection.rb', line 740 def date_valid_key_date = date_valid_parts[:key_date] |
#date_valid_parts ⇒ Object
691 |
# File 'lib/neu/mods/projection.rb', line 691 def date_valid_parts = date_parts("dateValid") |
#date_valid_precision ⇒ Object
736 |
# File 'lib/neu/mods/projection.rb', line 736 def date_valid_precision = date_valid_parts[:precision] |
#date_valid_qualifier ⇒ Object
739 |
# File 'lib/neu/mods/projection.rb', line 739 def date_valid_qualifier = date_valid_parts[:qualifier] |
#date_valid_text ⇒ Object
741 |
# File 'lib/neu/mods/projection.rb', line 741 def date_valid_text = date_valid_parts[:text] |
#digital_origin ⇒ Object
381 |
# File 'lib/neu/mods/projection.rb', line 381 def digital_origin = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:digitalOrigin", from: "..") |
#editable_corporate_creators ⇒ Object
329 330 331 |
# File 'lib/neu/mods/projection.rb', line 329 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.
323 324 325 326 327 |
# File 'lib/neu/mods/projection.rb', line 323 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 |
#edition ⇒ Object
401 |
# File 'lib/neu/mods/projection.rb', line 401 def edition = origin_texts_at("mods:edition") |
#extent ⇒ Object
380 |
# File 'lib/neu/mods/projection.rb', line 380 def extent = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:extent", from: "..") |
#format ⇒ Object
MODS puts @displayLabel on physicalDescription, not on the form, extent,
digitalOrigin, reformattingQuality or note inside it -- so these four
take the label off their parent. from: ".." says which element the
header comes from; the value still comes from the element itself.
379 |
# File 'lib/neu/mods/projection.rb', line 379 def format = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:form", from: "..") |
#frequency ⇒ Object
Serials. The @authority a record puts on a frequency is not projected: authority handling is a question the gem defers everywhere else -- for genre, subject and name -- and answering it for one field would be inconsistent.
456 |
# File 'lib/neu/mods/projection.rb', line 456 def frequency = origin_texts_at("mods:frequency") |
#genre_subjects ⇒ Object
234 |
# File 'lib/neu/mods/projection.rb', line 234 def genre_subjects = texts_at("/mods:mods/mods:subject/mods:genre") |
#genres ⇒ Object
A genre is a browse axis, so its entry carries the vocabulary the term came from. The other labeled fields do not: nothing gates on their vocabulary, and three more keys on fifteen fields is JSON no consumer reads.
387 |
# File 'lib/neu/mods/projection.rb', line 387 def genres = labeled_texts_at("/mods:mods/mods:genre", authority: true) |
#geographic_code_subjects ⇒ Object
A MARC GAC code. Projected as the record wrote it: turning it into a place name needs a lookup table, which is the same call the gem already made for MARC relators -- the label vocabulary belongs to the consumer.
239 |
# File 'lib/neu/mods/projection.rb', line 239 def geographic_code_subjects = texts_at("/mods:mods/mods:subject/mods:geographicCode") |
#geographic_subjects ⇒ Object
The other subject axes. Cerberus's IPTC ingest writes subject/geographic from the IPTC City and State fields, so this one was also being written on every batch and read back by nothing.
220 |
# File 'lib/neu/mods/projection.rb', line 220 def geographic_subjects = texts_at("/mods:mods/mods:subject/mods:geographic") |
#hierarchical_geographic_subjects ⇒ Object
Kept structured for the reason #map_data is. Flattening country / state / city into "United States -- New York (State) -- Parksville" would make a consumer that wants the city alone unpick a sentence.
This is the axis bdr_43888.mods.xml uses INSTEAD of subject/geographic, so that record projected no place at all -- a live ingest path, not a hypothetical.
255 256 257 258 259 260 |
# File 'lib/neu/mods/projection.rb', line 255 def hierarchical_geographic_subjects doc.xpath("/mods:mods/mods:subject/mods:hierarchicalGeographic", NAMESPACE).filter_map do |node| entry = HIERARCHICAL_GEOGRAPHIC_LEVELS.to_h { |level| [level, child_text(node, "mods:#{camelize(level)}")] } entry if entry.values.any? end end |
#host_collections ⇒ Object
The host's title plus THIS work's position within it. The host's own name, originInfo and identifier stay out: they belong to the other record, and a transcribed copy goes stale the moment that record is edited. A part is the exception, because a volume, issue and page range describe this article and no other record holds that fact.
An entry survives on its part alone. Requiring a title discarded the one piece of the block that was ours along with the metadata that never was, and how to render a titleless host is the consumer's call.
541 542 543 544 545 546 |
# File 'lib/neu/mods/projection.rb', line 541 def host_collections doc.xpath("/mods:mods/mods:relatedItem[@type='host']", NAMESPACE).filter_map do |node| entry = { title: child_text(node, "mods:titleInfo/mods:title"), **host_part(node) } entry.merge(qualifiers_of(node)) if entry.values.any? end end |
#identifiers ⇒ Object
{ type:, value: }, because a DOI, an accession number and a collection id are not the same kind of thing and no consumer can tell them apart from the digits alone -- a reader shown a bare 10.1234/x cannot see it is a DOI, and a display cannot decide to linkify it. The same argument #notes already makes for its @type, and #permanent_url already proves the attribute is load-bearing by special-casing @type='hdl'. cancelled, superseded or simply wrong. Projected flat, a dead ISBN read exactly like a live one and invited a reader to use it.
584 585 586 587 588 589 590 591 592 |
# File 'lib/neu/mods/projection.rb', line 584 def identifiers doc.xpath("/mods:mods/mods:identifier", NAMESPACE).filter_map do |node| value = clean(node.text) if value { type: clean(node["type"]), value: value, invalid: attr_value(node, "invalid") == "yes", **qualifiers_of(node) } end end end |
#issuance ⇒ Object
450 |
# File 'lib/neu/mods/projection.rb', line 450 def issuance = origin_texts_at("mods:issuance") |
#keywords ⇒ Object
The editable free-text keyword set (Cerberus simple form): topics under the attribute-free keyword subjects only.
158 159 160 |
# File 'lib/neu/mods/projection.rb', line 158 def keywords keyword_subjects.flat_map { |s| texts_under(s, "mods:topic") } end |
#languages ⇒ Object
{ term:, object_part:, script: } per language element. Prefer the
type="text" term, and translate a code-only one through the ISO 639
registry. A record saying eng projects "English", so the display and
the Solr language facet read the same value rather than the facet
showing codes. An unrecognised code survives as itself.
An entry rather than a bare string because @objectPart changes what the
record is claiming. <language objectPart="subtitles">spa says the
subtitles are Spanish, and projected flat it said the resource was --
which is the case a captioned video hits every time. The script rides
along for the same reason a name's role does: a consumer cannot
recover it from the term.
356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/neu/mods/projection.rb', line 356 def languages doc.xpath("/mods:mods/mods:language", NAMESPACE).filter_map do |lang| node = language_term_node(lang) term = language_term_of(node) next unless term # The authority comes off the <languageTerm> the term was read from, # never off the <language> around it: MODS carries @authority on the # term, and a code-only record declares `iso639-2b` there. { term: term, object_part: attr_value(lang, "objectPart"), script: script_term(lang), **(node), **qualifiers_of(lang) } end end |
#location ⇒ Object
location repeats, and one location mixes kinds: a shelf mark and a URL are not interchangeable, and a consumer has to know which it holds before it can decide to linkify it. So the parts stay apart.
The shelf mark is mods:shelfLocator. There is no shelfLocation element in MODS, and the spec fixture carried the same misspelling, so the field was unconditionally nil and the spec asserted nothing.
503 504 505 506 507 508 509 510 511 512 |
# File 'lib/neu/mods/projection.rb', line 503 def location doc.xpath("/mods:mods/mods:location", NAMESPACE).filter_map do |node| entry = { physical_location: child_text(node, "mods:physicalLocation"), shelf_location: child_text(node, "mods:shelfLocator"), url: child_text(node, "mods:url") } entry.merge(qualifiers_of(node)) if entry.values.any? end end |
#main_title ⇒ Object
Atlas names this field main_title; the registry requires a method per field name, and #access_title_parts is the descriptive name for what it returns. Kept as an alias rather than a rename so both read well.
961 |
# File 'lib/neu/mods/projection.rb', line 961 def main_title = access_title_parts |
#main_title_display_label ⇒ Object
What the record wants the title row headed, which is almost never set -- but a record that does set it means it, and "Title" is the one header a display would otherwise never let a curator change.
966 |
# File 'lib/neu/mods/projection.rb', line 966 def main_title_display_label = attr_value(primary_title_info, "displayLabel") |
#map_data ⇒ Object
subject/cartographics, kept structured. Composing "scale ; projection coordinates" into one string is display policy, and this gem does not own that -- a consumer that wants only the coordinates should not have to unpick a sentence to get them.
518 519 520 521 522 523 524 525 526 527 528 |
# File 'lib/neu/mods/projection.rb', line 518 def map_data doc.xpath("/mods:mods/mods:subject/mods:cartographics", NAMESPACE).filter_map do |node| entry = { scale: child_text(node, "mods:scale"), projection: child_text(node, "mods:projection"), coordinates: child_text(node, "mods:coordinates") } # cartographics carries neither attribute; the enclosing subject does. entry.merge(qualifiers_of(node.parent)) if entry.values.any? end end |
#name_entry(node) ⇒ Object
One name as the access copy wants it. affiliation is how a reader
tells one J. Doe from another, and it is the field an institutional
repository most wants: it repeats in the schema, so it is an array.
Added to the entry rather than as a parallel field, so a name and its affiliation cannot be zipped together wrongly by a consumer.
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/neu/mods/projection.rb', line 270 def name_entry(node) { name: name_display_value_w_date(node), roles: name_roles(node), affiliation: texts_under(node, "mods:affiliation"), # @usage is fixed="primary" in the schema and exists to nominate the # principal name. A record that sets it has said which name leads, # and without it a consumer grouping role-less names can only guess. usage: attr_value(node, "usage"), alternative_names: alternative_names(node), # The vocabulary this name was taken from, read off the <name> alone. # A marcrelator <roleTerm> inside it says what the person DID, not # which list the name came from -- see #authority_of. **(node), **qualifiers_of(node) } end |
#names ⇒ Object
All top-level names as { name:, roles: }. name reproduces the mods gem's
display_value_w_date (including its quirks -- faithfully, so existing Solr/
display output is preserved). MODS repeats role on one name, and a
cataloguer who records that a person both wrote and edited a work means
both. Each term prefers the type="text" roleTerm, falling back to the raw
code (NOT MARC-relator-translated -- see README).
A name with no name text drops out. A mods:name carrying only a role projected { name: nil, roles: ["edt"] }, which a display renders as a labelled empty row and which every consumer had to guard against with its own compact_blank. #preserved_names deliberately keeps it: that list tells a curator what the XML holds, so an element they need to fix has to stay visible there.
313 314 315 316 317 318 |
# File 'lib/neu/mods/projection.rb', line 313 def names doc.xpath("/mods:mods/mods:name", NAMESPACE).filter_map do |node| entry = name_entry(node) entry if entry[:name] end end |
#notes ⇒ Object
Every top-level note, keeping its @type. The type carries meaning -- a "statement of responsibility" is not a "funding" note -- so flattening them into bare strings would repeat the accessCondition mistake.
489 490 491 492 493 494 |
# File 'lib/neu/mods/projection.rb', line 489 def notes doc.xpath("/mods:mods/mods:note", NAMESPACE).filter_map do |node| value = clean(node.text) { type: clean(node["type"]), value: value, **qualifiers_of(node) } if value end end |
#occupation_subjects ⇒ Object
The last unprojected member of a closed set: every other subject child already has a field, so leaving this one out made "what a subject can carry" arbitrary rather than complete.
232 |
# File 'lib/neu/mods/projection.rb', line 232 def occupation_subjects = texts_at("/mods:mods/mods:subject/mods:occupation") |
#origin_agents ⇒ Object
originInfo/agent, new in MODS 3.8: who performed the event the block records. Read through the same port as a top-level name, so a publisher recorded as an agent composes the way a creator does and carries its roles -- which is what a consumer heads the row with when the block states no displayLabel or eventType.
433 434 435 436 437 438 |
# File 'lib/neu/mods/projection.rb', line 433 def origin_agents doc.xpath("/mods:mods/mods:originInfo/mods:agent", NAMESPACE).filter_map do |node| entry = name_entry(node) entry.merge(event_type: attr_value(node.parent, "eventType")) if entry[:name] end end |
#permanent_url ⇒ Object
594 595 596 597 |
# File 'lib/neu/mods/projection.rb', line 594 def permanent_url node = doc.at_xpath("/mods:mods/mods:identifier[@type='hdl']", NAMESPACE) node && clean(node.text) end |
#permanent_url_display_label ⇒ Object
The handle identifier carries @displayLabel="Permanent URL" in Atlas's own MODS template, so the header a reader sees is one the record states rather than one a decorator invents. No href companion: the value is the URL.
603 604 605 |
# File 'lib/neu/mods/projection.rb', line 603 def permanent_url_display_label attr_value(doc.at_xpath("/mods:mods/mods:identifier[@type='hdl']", NAMESPACE), "displayLabel") end |
#personal_name_subjects ⇒ Object
Name subjects compose through the same display-value port as #names, so one person reads the same whether they authored the work or are its subject.
226 |
# File 'lib/neu/mods/projection.rb', line 226 def personal_name_subjects = name_subjects("personal") |
#physical_description_notes ⇒ Object
A note about the object rather than about the work -- "Scanned at 600 dpi" belongs beside the extent, not beside a content note. Projected as plain strings like its physicalDescription siblings: #notes keeps @type because the type changes what a top-level note means, and nothing here turns on it.
478 |
# File 'lib/neu/mods/projection.rb', line 478 def physical_description_notes = labeled_texts_at("/mods:mods/mods:physicalDescription/mods:note", from: "..") |
#place_of_publication ⇒ Object
418 419 420 421 422 423 424 425 426 |
# File 'lib/neu/mods/projection.rb', line 418 def place_of_publication doc.xpath("/mods:mods/mods:originInfo/mods:place", NAMESPACE).filter_map do |place| value = place_term_value(place) next unless value { value: value, **origin_qualifiers_of(place.parent), date_elements: origin_date_elements(place.parent) } end end |
#place_term_value(place) ⇒ Object
440 441 442 443 444 445 446 447 448 |
# File 'lib/neu/mods/projection.rb', line 440 def place_term_value(place) text = clean(place.at_xpath("mods:placeTerm[@type='text']", NAMESPACE)&.text) return text if text code = place.at_xpath("mods:placeTerm", NAMESPACE) return nil if attr_value(code, "authority") == MARC_COUNTRY_AUTHORITY clean(code&.text) end |
#plain_title ⇒ Object
Composed display title (the former Atlas MODSDecoration#plain_title), driven off the scoped primary title.
45 46 47 |
# File 'lib/neu/mods/projection.rb', line 45 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 + roles, like #names but filtered to the preserved set.
336 337 338 339 340 |
# File 'lib/neu/mods/projection.rb', line 336 def preserved_names doc.xpath("/mods:mods/mods:name", NAMESPACE) .reject { |node| editable_creator_name?(node) } .map { |node| name_entry(node) } end |
#publication_information ⇒ Object
originInfo repeats, and so do publisher and edition within one. Cerberus's IPTC ingest writes the publisher from the IPTC Source field on every batch, so this element was being written into the preservation XML and then read back by nothing. place, edition, issuance or frequency inside it, so each of these takes its header off the parent block.
400 |
# File 'lib/neu/mods/projection.rb', line 400 def publication_information = origin_texts_at("mods:publisher") |
#record_info ⇒ Object
Who catalogued this record, to what standard, and when. It describes the CATALOGUING rather than the resource, which is why it is one value and why a consumer is unlikely to want it beside Publisher -- but dropping a preservation repository's provenance statement on read is wrong on its face, so it is projected and the display question is the consumer's.
777 778 779 780 781 782 783 |
# File 'lib/neu/mods/projection.rb', line 777 def record_info node = doc.at_xpath("/mods:mods/mods:recordInfo", NAMESPACE) return nil unless node entry = RECORD_INFO_PARTS.transform_values { |xpath| child_text(node, xpath) } entry if entry.values.any? end |
#reformatting_quality ⇒ Object
469 470 471 |
# File 'lib/neu/mods/projection.rb', line 469 def reformatting_quality labeled_texts_at("/mods:mods/mods:physicalDescription/mods:reformattingQuality", from: "..") end |
#related_items ⇒ Object
Every other relatedItem, keeping its @type. MODS also defines constituent, otherFormat, original, preceding, succeeding, isReferencedBy and reviewOf, and a record carrying any of them projected nothing at all. The type rides along because "the print edition" and "reviewed in" are not the same relationship, and no consumer can recover which it holds from the title alone. An untyped relatedItem lands here with a nil type.
565 566 567 568 569 570 571 572 573 |
# File 'lib/neu/mods/projection.rb', line 565 def doc.xpath("/mods:mods/mods:relatedItem", NAMESPACE).filter_map do |node| type = clean(node["type"]) next if NAMED_RELATED_ITEM_TYPES.include?(type) title = clean(node.at_xpath("mods:titleInfo/mods:title", NAMESPACE)&.text) { type: type, title: title, **qualifiers_of(node) } if title end end |
#related_series ⇒ Object
530 |
# File 'lib/neu/mods/projection.rb', line 530 def = ("series") |
#resource_type ⇒ Object
MODS repeats typeOfResource, and repeats physicalDescription (and form and extent within one), so all four are :many. A record that is both text and a still image used to project as text alone.
373 |
# File 'lib/neu/mods/projection.rb', line 373 def resource_type = labeled_texts_at("/mods:mods/mods:typeOfResource") |
#restriction_on_access ⇒ Object
128 |
# File 'lib/neu/mods/projection.rb', line 128 def restriction_on_access = access_conditions_of_type("restriction on access") |
#restriction_on_access_display_label ⇒ Object
142 143 144 |
# File 'lib/neu/mods/projection.rb', line 142 def restriction_on_access_display_label first_attr(access_condition_nodes("restriction on access"), "displayLabel") end |
#restriction_on_access_href ⇒ Object
146 |
# File 'lib/neu/mods/projection.rb', line 146 def restriction_on_access_href = first_href(access_condition_nodes("restriction on access")) |
#subject_headings ⇒ Object
Every top-level
The parts stay, because the Advanced edit form and a consumer wanting
one step of a heading both ask for them. heading: is the same parts
joined, and it is here rather than left to each caller because the
composed heading is now BOTH the string a display renders and the
string a browse index holds. Two callers joining independently is how
a displayed value and an indexed value drift apart, and this join
makes them the same string rather than two that happen to match.
axis: names the MODS element the heading's MAIN term came from, which
is the first child that carries heading text. A consumer cannot derive
it from the parts -- they are bare strings -- and it is the fact that
says which browse a heading belongs to: "Salt marshes -- Massachusetts"
is a topic heading with a place subdivision, not a place. Reported as
the element name, so the browse vocabulary stays with the consumer.
202 203 204 205 206 207 208 209 210 211 |
# File 'lib/neu/mods/projection.rb', line 202 def subject_headings doc.xpath("/mods:mods/mods:subject", NAMESPACE).filter_map do |node| axis_node = heading_axis_node(node) parts = subject_heading_parts(node) next if axis_node.nil? || parts.empty? { parts: parts, heading: parts.join(HEADING_SEPARATOR), axis: heading_axis(axis_node), **(axis_node), **qualifiers_of(node) } end end |
#table_of_contents ⇒ Object
Read with its line breaks intact. A legacy contents list separates its entries by newline, and the whitespace collapse every other field wants ran the entries together into one line -- there the break IS the structure, not stray formatting. A "--"-separated list is unaffected.
462 463 464 465 466 467 |
# File 'lib/neu/mods/projection.rb', line 462 def table_of_contents doc.xpath("/mods:mods/mods:tableOfContents", NAMESPACE).filter_map do |node| lines = NEU::MODS.canonical_lines(node.text) labeled(lines, node) unless lines.empty? end end |
#target_audience ⇒ Object
Who the resource is for. The last displayed top-level element with no projection at all: a record naming its audience said so to nobody.
391 |
# File 'lib/neu/mods/projection.rb', line 391 def target_audience = labeled_texts_at("/mods:mods/mods:targetAudience") |
#temporal_subjects ⇒ Object
221 |
# File 'lib/neu/mods/projection.rb', line 221 def temporal_subjects = texts_at("/mods:mods/mods:subject/mods:temporal") |
#title_parts ⇒ Object
Structured primary-title parts, byte-faithful to the document. nil for an absent part (the Cerberus form treats nil as "not present"); to_h coerces to "" for the Atlas main_title.
Faithful on purpose: this is what Cerberus pre-fills its edit forms from (MODSFields for the Metadata tab, load_advanced! for the Advanced tab), and MODSMerge writes back whatever the form posts. Normalising here would rewrite the curator's characters in the preservation XML on the next save. #access_title_parts is the normalised surface.
29 30 31 |
# File 'lib/neu/mods/projection.rb', line 29 def title_parts title_parts_of(primary_title_info) end |
#title_subjects ⇒ Object
A subject that is a work has a nonSort, a subTitle and part numbers like any other titleInfo, so it composes through the same port as the main title rather than taking titleInfo/title alone.
244 245 246 |
# File 'lib/neu/mods/projection.rb', line 244 def title_subjects doc.xpath("/mods:mods/mods:subject/mods:titleInfo", NAMESPACE).filter_map { |node| composed_title_of(node) } 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.
941 942 943 |
# File 'lib/neu/mods/projection.rb', line 941 def to_h FIELDS.keys.to_h { |field| [field, public_send(field)] } end |
#topical_subjects ⇒ Object
Every
215 |
# File 'lib/neu/mods/projection.rb', line 215 def topical_subjects = texts_at("/mods:mods/mods:subject/mods:topic") |
#translated_title ⇒ Object
40 |
# File 'lib/neu/mods/projection.rb', line 40 def translated_title = variant_titles("translated") |
#uniform_title ⇒ Object
39 |
# File 'lib/neu/mods/projection.rb', line 39 def uniform_title = variant_titles("uniform") |
#use_and_reproduction ⇒ Object
The two @type values MODS defines, projected apart. Collapsing them into one value presented an access restriction to a reader as a licence, which is the one defect in this area that misinforms someone about their rights rather than merely hiding a field.
127 |
# File 'lib/neu/mods/projection.rb', line 127 def use_and_reproduction = access_conditions_of_type("use and reproduction") |
#use_and_reproduction_display_label ⇒ Object
136 137 138 |
# File 'lib/neu/mods/projection.rb', line 136 def use_and_reproduction_display_label first_attr(access_condition_nodes("use and reproduction"), "displayLabel") end |
#use_and_reproduction_href ⇒ Object
140 |
# File 'lib/neu/mods/projection.rb', line 140 def use_and_reproduction_href = first_href(access_condition_nodes("use and reproduction")) |