require_relative 'converter' require 'set' class EADConverter < Converter require 'securerandom' require_relative 'lib/xml_sax' include ASpaceImport::XML::SAX def self.import_types(show_hidden = false) [ { :name => "ead_xml", :description => "Import EAD records from an XML file" } ] end def self.instance_for(type, input_file) if type == "ead_xml" self.new(input_file) else nil end end # A lot of nodes need tweaking to format the content. Like, people love their p's but they don't # actually want to ever see them. def format_content(content) return content if content.nil? content.tr!("\n", ' ') # literal linebreaks are assumed to not be part of data content.gsub(%r{/]*)?>}, "").gsub(%r{

|]*)?/>}, "\n\n") .gsub("", "\n\n").gsub("", "\n\n").gsub("", "") .strip end # alright, wtf. # sometimes notes can have things like lists jammed in them. we need to break those # out, but keep the narrative order of the notes. def insert_into_subnotes(split_tag = 'list') subnotes = ancestor(:note_multipart).subnotes theleftovers = nil unless subnotes.nil? if subnotes.is_a?(Array) sn = subnotes.pop else sn = subnotes end if sn["content"] # clone the object... theleftovers = sn.dup # rip out the list, and put the left overs back in the content content = sn["content"].gsub("ead:#{split_tag}", split_tag) # just in case.. sn["content"], trash, theleftovers["content"] = content.partition(/<#{split_tag}[^>]*>.*?<\/#{split_tag}>/m) # what a hack. ripping out the list might leave some dangling

s [sn, theleftovers].each do |s| next if s["content"].nil? s["content"] = Nokogiri::XML::DocumentFragment.parse(s["content"].strip.gsub(/^<\/p[^>]*>/, '')).to_xml(:encoding => 'utf-8') end end # put everything before the list back... unless ( sn["content"].nil? or sn["content"].length < 1 ) set ancestor(:note_multipart), :subnotes, sn end end # now return the leftovers to be delt with after the list subnote has # been created theleftovers end def self.configure with 'ead' do |*| make :resource, { :publish => att('audience') != 'internal', :finding_aid_language => 'und', :finding_aid_script => 'Zyyy' } end ignore "titlepage" # addresses https://archivesspace.atlassian.net/browse/AR-1282 with 'eadheader' do |*| set :finding_aid_status, att('findaidstatus') end with 'archdesc' do |*| publish = if !context_obj.publish || (att('audience') == 'internal') false else true end set :level, att('level') || 'otherlevel' set :other_level, att('otherlevel') set :publish, publish end # c, c1, c2, etc... (0..12).to_a.map {|i| "c" + (i+100).to_s[1..-1]}.push('c').each do |c| with c do |*| make :archival_object, { :level => att('level') || 'otherlevel', :other_level => att('otherlevel'), :ref_id => att('id'), :resource => ancestor(:resource), :parent => ancestor(:archival_object), :publish => att('audience') != 'internal' } end end with 'unitid' do |node| extract_ark = proc do |s| if s.start_with?(' node.attribute( "type"), :external_id => inner_xml } do |ext_id| set ancestor(:resource ), :external_ids, ext_id end end when 'archival_object' set obj, :component_id, inner_xml if obj.component_id.nil? || obj.component_id.empty? if node.attribute( "type" ) make :external_id, { :source => node.attribute( "type" ), :external_id => inner_xml } do |ext_id| set ancestor(:resource, :archival_object), :external_ids, ext_id end end end end end end with 'unittitle' do |node| ancestor(:note_multipart, :resource, :archival_object) do |obj| unless obj.class.record_type == "note_multipart" title = Nokogiri::XML::DocumentFragment.parse(inner_xml.strip) title.xpath(".//unitdate").remove obj.title = format_content( title.to_xml(:encoding => 'utf-8') ) if obj.title.nil? || obj.title.empty? end end end with 'unitdate' do |node| norm_dates = (att('normal') || "").sub(/^\s/, '').sub(/\s$/, '').split('/') # why were the next 3 lines added? removed for now, since single dates can stand on their own. #if norm_dates.length == 1 # norm_dates[1] = norm_dates[0] #end norm_dates.map! {|d| d =~ /^([0-9]{4}(\-(1[0-2]|0[1-9])(\-(0[1-9]|[12][0-9]|3[01]))?)?)$/ ? d : nil} make :date, { :date_type => att('type') || ( norm_dates[1] ? 'inclusive' : 'single' ), :expression => inner_xml, :label => 'creation', :begin => norm_dates[0], :end => norm_dates[1], :calendar => att('calendar'), :era => att('era'), :certainty => att('certainty') } do |date| set ancestor(:resource, :archival_object), :dates, date end end with "archdesc/note" do |*| make :note_multipart, { :type => 'odd', :persistent_id => att('id'), :publish => att('audience') != 'internal', :subnotes => { :publish => att('audience') != 'internal', 'jsonmodel_type' => 'note_text', 'content' => format_content( inner_xml ) } } do |note| set ancestor(:resource, :archival_object), :notes, note end end with 'langmaterial' do |*| langmaterial = Nokogiri::XML::DocumentFragment.parse(inner_xml) ancestor(:resource, :archival_object) do |obj| # if contains encoded tags create a matching language_and_script record if !(languages = langmaterial.xpath('.//language')).empty? && langmaterial.xpath('.//language').any? { |l| l.attr('langcode') } languages.each do |language| next unless (langcode = language.attr('langcode')) script = language.attr('scriptcode') make :lang_material, { :jsonmodel_type => 'lang_material', :language_and_script => { 'jsonmodel_type' => 'language_and_script', 'language' => langcode.to_s, 'script' => script ? script.to_s : nil } } do |lang| set obj, :lang_materials, lang end end # if a resource and no set to undetermined elsif obj.class.record_type == 'resource' make :lang_material, { :jsonmodel_type => 'lang_material', :language_and_script => { 'jsonmodel_type' => 'language_and_script', 'language' => 'und' } } do |lang| set obj, :lang_materials, lang end end # write full content to a note, subbing out the language tags (if present) langmaterial.search('.//language').each do |node| node.replace Nokogiri::XML::Text.new(node.inner_html, node.document) end content = langmaterial.to_s unless content.nil? || content.strip.empty? make :lang_material, { :jsonmodel_type => 'lang_material', :notes => { 'jsonmodel_type' => 'note_langmaterial', 'type' => 'langmaterial', 'persistent_id' => att('id'), 'publish' => att('audience') != 'internal', 'content' => [format_content( content.sub(/.*?<\/head>/, '') )] } } do |note| set obj, :lang_materials, note end end end end # If we've gotten this far and still haven't hit a we must assign an undetermined language value with "archdesc/did" do |e| if context_obj['jsonmodel_type'] == 'resource' && inner_xml.include?(' 'lang_material', :language_and_script => { 'jsonmodel_type' => 'language_and_script', 'language' => 'und' } } do |lang| set ancestor(:resource), :lang_materials, lang break end end end def make_single_note(note_name, tag, tag_name="") content = tag.inner_text if !tag_name.empty? content = tag_name + ": " + content end make :note_singlepart, { :type => note_name, :persistent_id => att('id'), :label => att('label'), :publish => att('audience') != 'internal', :content => format_content( content.sub(/.?<\/head>/, '').strip) } do |note| set ancestor(:resource, :archival_object), :notes, note end end def make_nested_note(note_name, tag) content = tag.inner_text make :note_multipart, { :type => note_name, :persistent_id => att('id'), :label => att('label'), :publish => att('audience') != 'internal', :subnotes => { :publish => att('audience') != 'internal', 'jsonmodel_type' => 'note_text', 'content' => format_content( content ) } } do |note| set ancestor(:resource, :archival_object), :notes, note end end with 'physdesc' do |*| physdesc = Nokogiri::XML::DocumentFragment.parse(inner_xml) extent_number_and_type = nil dimensions = [] physfacets = [] container_summaries = [] other_extent_data = [] container_summary_texts = [] dimensions_texts = [] physfacet_texts = [] # If there is already a portion of 'part' specified, use it if att('altrender') && att('altrender').downcase == 'part' portion = 'part' else portion = 'whole' end # Special case: if the physdesc is just a plain string with no child elements, treat its contents as a physdesc note if physdesc.children.length == 1 && physdesc.children[0].name == 'text' container_summaries << physdesc else # Otherwise, attempt to parse out an extent record from the child elements. physdesc.children.each do |child| # "extent" can have one of two kinds of semantic meanings: either a true extent with number and type, # or a container summary. Disambiguation is done through a regex. if child.name == 'extent' child_content = child.content.strip if extent_number_and_type.nil? && child_content =~ /^([0-9\.]+)+\s+(.*)$/ extent_number_and_type = {:number => $1, :extent_type => $2} else container_summaries << child container_summary_texts << child.content.strip end elsif child.name == 'physfacet' physfacets << child physfacet_texts << child.content.strip elsif child.name == 'dimensions' dimensions << child dimensions_texts << child.content.strip elsif child.name != 'text' other_extent_data << child end end end # only make an extent if we got a number and type, otherwise put all tags in the physdesc in new notes if extent_number_and_type make :extent, { :number => $1, :extent_type => $2, :portion => portion, :container_summary => container_summary_texts.join('; '), :physical_details => physfacet_texts.join('; '), :dimensions => dimensions_texts.join('; ') } do |extent| set ancestor(:resource, :archival_object), :extents, extent end # there's no true extent; split up the rest into individual notes else container_summaries.each do |summary| make_single_note("physdesc", summary) end physfacets.each do |physfacet| make_single_note("physfacet", physfacet) end dimensions.each do |dimension| make_nested_note("dimensions", dimension) end end other_extent_data.each do |unknown_tag| make_single_note("physdesc", unknown_tag, unknown_tag.name) end end with 'bibliography' do |*| make :note_bibliography set :persistent_id, att('id') set :publish, att('audience') != 'internal' set ancestor(:resource, :archival_object), :notes, proxy end with 'index' do |*| make :note_index set :persistent_id, att('id') set :publish, att('audience') != 'internal' set ancestor(:resource, :archival_object), :notes, proxy end %w(bibliography index).each do |x| with "#{x}/head" do |node| set :label, format_content( inner_xml ) end with "#{x}/p" do |*| set :content, format_content( inner_xml ) end end with 'bibliography/bibref' do |*| set :items, inner_xml end # Multiple elements within one indexentry are generally related # Parse the indexentry as a fragment, and map the child elements # to ASpace equivalents, according to this mapping: field_mapping = { 'name' => 'name', 'persname' => 'person', 'famname' => 'family', 'corpname' => 'corporate_entity', 'subject' => 'subject', 'function' => 'function', 'occupation' => 'occupation', 'genreform' => 'genre_form', 'title' => 'title', 'geogname' => 'geographic_name', } with 'indexentry' do |*| entry_type = '' entry_value = '' entry_reference = '' entry_ref_target = '' indexentry = Nokogiri::XML::DocumentFragment.parse(inner_xml) indexentry.children.each do |child| if field_mapping.key? child.name entry_value << child.content entry_type << field_mapping[child.name] elsif child.name == 'ref' && child.xpath('./ptr').count == 0 entry_reference << child.content entry_ref_target << (child['target'] || '') elsif child.name == 'ref' entry_reference = format_content( child.inner_html ) end end make :note_index_item, { :type => entry_type, :value => entry_value, :reference_text => entry_reference, :reference => entry_ref_target } do |item| set ancestor(:note_index), :items, item end end %w(accessrestrict accessrestrict/legalstatus accruals acqinfo altformavail appraisal arrangement bioghist custodhist fileplan odd otherfindaid originalsloc phystech prefercite processinfo relatedmaterial scopecontent separatedmaterial userestrict ).each do |note| with note do |node| content = inner_xml.tap {|xml| xml.sub!(/.*?<\/head>/m, '') # xml.sub!(/]*>.*?<\/list>/m, '') # xml.sub!(/]*>.*<\/chronlist>/m, '') } make :note_multipart, { :type => node.name, :persistent_id => att('id'), :publish => att('audience') != 'internal', :subnotes => { :publish => att('audience') != 'internal', 'jsonmodel_type' => 'note_text', 'content' => format_content( content ) } } do |note| set ancestor(:resource, :archival_object), :notes, note end end end %w(abstract materialspec physloc).each do |note| with note do |node| content = inner_xml make :note_singlepart, { :type => note, :persistent_id => att('id'), :publish => att('audience') != 'internal', :content => format_content( content.sub(/.*?<\/head>/, '') ) } do |note| set ancestor(:resource, :archival_object), :notes, note end end end with 'notestmt/note' do |*| append :finding_aid_note, format_content( inner_xml ) end with 'chronlist' do |*| if ancestor(:note_multipart) left_overs = insert_into_subnotes('chronlist') else left_overs = nil make :note_multipart, { :type => node.name, :persistent_id => att('id'), :publish => att('audience') != 'internal' } do |note| set ancestor(:resource, :archival_object), :notes, note end end make :note_chronology, { :publish => att('audience') != 'internal' } do |note| set ancestor(:note_multipart), :subnotes, note end # and finally put the leftovers back in the list of subnotes... if ( !left_overs.nil? && left_overs["content"] && left_overs["content"].length > 0 ) set ancestor(:note_multipart), :subnotes, left_overs end end with 'chronitem' do |*| context_obj.items << {} end %w(eventgrp/event chronitem/event).each do |path| with path do |*| context_obj.items.last['events'] ||= [] context_obj.items.last['events'] << format_content( inner_xml ) end end with 'list' do |*| if ancestor(:note_multipart) left_overs = insert_into_subnotes else left_overs = nil make :note_multipart, { :type => 'odd', :persistent_id => att('id'), :publish => att('audience') != 'internal' } do |note| set ancestor(:resource, :archival_object), :notes, note end end # now let's make the subnote list type = att('type') if type == 'deflist' || (type.nil? && inner_xml.match(//)) make :note_definedlist, { :publish => att('audience') != 'internal' } do |note| set ancestor(:note_multipart), :subnotes, note end else make :note_orderedlist, { :enumeration => att('numeration'), :publish => att('audience') != 'internal' } do |note| set ancestor(:note_multipart), :subnotes, note end end # and finally put the leftovers back in the list of subnotes... if ( !left_overs.nil? && left_overs["content"] && left_overs["content"].length > 0 ) set ancestor(:note_multipart), :subnotes, left_overs end end with 'list/head' do |node| set :title, format_content( inner_xml ) end with 'defitem' do |node| context_obj.items << {} end with 'defitem/label' do |node| context_obj.items.last['label'] = format_content( inner_xml ) if context == :note_definedlist end with 'defitem/item' do |node| context_obj.items.last['value'] = format_content( inner_xml ) if context == :note_definedlist end with 'list/item' do |*| set :items, inner_xml if context == :note_orderedlist end with 'publicationstmt/date' do |*| set :finding_aid_date, inner_xml if context == :resource end with 'date' do |*| if context == :note_chronology date = inner_xml context_obj.items.last['event_date'] = date end end with 'head' do |*| if context == :note_multipart set :label, format_content( inner_xml ) elsif context == :note_chronology set :title, format_content( inner_xml ) end end def remember_instance(instance, id = nil) @instances ||= {} @instances[id] = instance if id @last_instance = instance end def recall_instance(id = nil) id ? @instances[id] : @last_instance end def add_to_instance(type, indicator, id, parent_id = nil) if (instance = recall_instance(parent_id)) sub_container = instance.sub_container if sub_container['type_3'] # trying to add to a full sub_container - this shouldn't happen else level = sub_container["type_2"].nil? ? "2" : "3" sub_container["type_#{level}"] = type sub_container["indicator_#{level}"] = indicator # remember this one because someone might be adding to it remember_instance(instance, id) end else # can't find the instance to add to - this shouldn't happen end end def get_or_make_top_container_uri(type, indicator, barcode, container_profile_name) # remember the top_containers we make in this hash # the values are top_container uris # the keys are barcodes or type:indicator # some assumptions: # - barcodes are unique in this repo # - a barcode will never look like a type:indicator # - type:indicator is not unique # but only the last one seen will need to be added to # so it's actually a blessing that prior ones get blatted @top_container_uris ||= {} if barcode if @top_container_uris[barcode] return @top_container_uris[barcode] elsif (TopContainer.for_barcode(barcode) && TopContainer.for_barcode(barcode).uri) return TopContainer.for_barcode(barcode).uri end elsif @top_container_uris["#{type}:#{indicator}"] return @top_container_uris["#{type}:#{indicator}"] end # don't make a container_profile, but link to one if there's a match container_profile = ContainerProfile.filter(:name => container_profile_name).first make :top_container, { :barcode => barcode, :indicator => indicator, :type => type } do |top_container| if container_profile set top_container, :container_profile, {:ref => container_profile.uri} end end if barcode @top_container_uris[barcode] = context_obj.uri else @top_container_uris["#{type}:#{indicator}"] = context_obj.uri end context_obj.uri end with 'container' do |*| if context == :instance # this container is nested inside the last one # so add to the current sub_container # note: there is not an example of this in: # backend/app/exporters/examples/ead/ # but the previous implementation supported it # so continuing support here add_to_instance(att('type'), format_content(inner_xml), att('id')) return end if att('parent') # this container has a parent attribute # so there should have been a sub_container previously # with that id that we can add to add_to_instance(att('type'), format_content(inner_xml), att('id'), att('parent')) return end if !att('id') && defined?(context_obj.instances) && (instance = context_obj.instances.last) # this container doesn't have an @id # and has a container sibling before it # so even though it doesn't have a parent attribute # it is treated as a child of the prior sibling # this pattern is seen in the wnyu.xml example # it is necessary to test for @id because in vmi.xml a list # of sibling containers represents more than one instance add_to_instance(att('type'), format_content(inner_xml), att('id')) return end # all of the cases that require adding to an existing sub_container # are now handled, so having arrived here it is necessary to # create a new instance with a sub_container instance_type = att('label') || 'mixed_materials' if instance_type =~ /(.*)\s+?[\(\[]\s*(.*?)\s*[\)\]]$/ instance_type = $1 barcode = $2 end make :instance, { :instance_type => instance_type.downcase.strip } do |instance| set ancestor(:resource, :archival_object), :instances, instance end instance = context_obj top_container_uri = get_or_make_top_container_uri(att('type'), format_content(inner_xml), barcode, att("altrender")) make :sub_container, { :top_container => {'ref' => top_container_uri} } do |sub_container| set instance, :sub_container, sub_container end # remember the instance as it might be necessary to add to it later remember_instance(instance, att('id')) end with 'author' do |*| set :finding_aid_author, inner_xml end with 'descrules' do |*| set :finding_aid_description_rules, format_content( inner_xml ) end with 'eadid' do |*| set :ead_id, inner_xml set :ead_location, att('url') end with 'editionstmt' do |*| set :finding_aid_edition_statement, format_content( inner_xml ) end with 'seriesstmt' do |*| set :finding_aid_series_statement, format_content( inner_xml ) end with 'sponsor' do |*| set :finding_aid_sponsor, format_content( inner_xml ) end with 'titleproper' do |*| type = att('type') case type when 'filing' set :finding_aid_filing_title, format_content( inner_xml ) else set :finding_aid_title, format_content( inner_xml ) end end with 'subtitle' do |*| set :finding_aid_subtitle, format_content( inner_xml ) end with 'profiledesc' do |*| profiledesc = Nokogiri::XML::DocumentFragment.parse(inner_xml) if !(langusage = profiledesc.xpath(".//langusage")).empty? # If there is a langcode attribute inside a element, set the finding_aid_language to that langcode and finding_aid_note to full element content if (language = langusage.xpath('.//language')).size != 0 && (langcode = langusage.xpath('.//language').attr('langcode')) set :finding_aid_language, langcode.to_s if (script = language.attr('scriptcode')) set :finding_aid_script, script.to_s end end set :finding_aid_language_note, format_content( langusage.inner_text ) # if no , set language to undetermined else set :finding_aid_language, 'und' end end with 'revisiondesc/change' do |*| make :revision_statement set ancestor(:resource), :revision_statements, proxy set :publish, !(att('audience') === 'internal') end with 'revisiondesc/change/item' do |*| set :description, format_content( inner_xml ) end with 'revisiondesc/change/date' do |*| set :date, format_content( inner_xml ) end with 'origination/corpname' do |*| make_corp_template(:role => 'creator') end with 'controlaccess/corpname' do |*| make_corp_template(:role => 'subject') end with 'origination/famname' do |*| make_family_template(:role => 'creator') end with 'controlaccess/famname' do |*| make_family_template(:role => 'subject') end with 'origination/persname' do |*| make_person_template(:role => 'creator') end with 'controlaccess/persname' do |*| make_person_template(:role => 'subject') end { 'function' => 'function', 'genreform' => 'genre_form', 'geogname' => 'geographic', 'occupation' => 'occupation', 'subject' => 'topical', 'title' => 'uniform_title' }.each do |tag, type| with "controlaccess/#{tag}" do |*| make :subject, { :terms => {'term' => inner_xml, 'term_type' => type, 'vocabulary' => '/vocabularies/1'}, :vocabulary => '/vocabularies/1', :source => att('source') || 'ingest' } do |subject| set ancestor(:resource, :archival_object), :subjects, {'ref' => subject.uri} end end end with 'dao' do |*| make :instance, { :instance_type => 'digital_object' } do |instance| set ancestor(:resource, :archival_object), :instances, instance end make :digital_object, { :digital_object_id => SecureRandom.uuid, :publish => att('audience') != 'internal', :title => att('title') } do |obj| obj.file_versions << { :use_statement => att('role'), :file_uri => att('href'), :xlink_actuate_attribute => att('actuate'), :xlink_show_attribute => att('show'), :publish => att('audience') != 'internal', } set ancestor(:instance), :digital_object, obj end end with 'daodesc' do |*| make :note_digital_object, { :type => 'note', :persistent_id => att('id'), :publish => att('audience') != 'internal', :content => inner_xml.strip } do |note| set ancestor(:digital_object), :notes, note end end with 'daogrp' do |*| title = att('title') unless title title = '' ancestor(:resource, :archival_object ) { |ao| display_string = ArchivalObject.produce_display_string(ao) display_string = Nokogiri::XML::DocumentFragment.parse(display_string).inner_text title << display_string + ' Digital Object' } end make :digital_object, { :digital_object_id => SecureRandom.uuid, :title => title, :publish => att('audience') != 'internal' } do |obj| ancestor(:resource, :archival_object) do |ao| ao.instances.push({'instance_type' => 'digital_object', 'digital_object' => {'ref' => obj.uri}}) end # Actuate and Show values applicable to s can come from elements, # so daogrp contents need to be handled together dg_contents = Nokogiri::XML::DocumentFragment.parse(inner_xml) # Hashify arc attrs keyed by xlink:to arc_by_to_val = dg_contents.xpath('arc').map {|arc| if arc['xlink:to'] [arc['xlink:to'], arc] else nil end }.reject(&:nil?).reduce({}) {|hsh, (k, v)| hsh[k] = v; hsh} dg_contents.xpath('daoloc').each do |daoloc| arc = arc_by_to_val[daoloc['xlink:label']] || {} fv_attrs = {} # attrs on fv_attrs[:xlink_show_attribute] = arc['xlink:show'] if arc['xlink:show'] fv_attrs[:xlink_actuate_attribute] = arc['xlink:actuate'] if arc['xlink:actuate'] # attrs on fv_attrs[:file_uri] = daoloc['xlink:href'] if daoloc['xlink:href'] fv_attrs[:use_statement] = daoloc['xlink:role'] if daoloc['xlink:role'] fv_attrs[:publish] = daoloc['audience'] != 'internal' obj.file_versions << fv_attrs end obj end end end # Templates Section def make_corp_template(opts) return nil if inner_xml.strip.empty? make :agent_corporate_entity, { :agent_type => 'agent_corporate_entity', :publish => att('audience') == 'external' ? true : false } do |corp| set ancestor(:resource, :archival_object), :linked_agents, {'ref' => corp.uri, 'role' => opts[:role], 'relator' => att('role')} end make :name_corporate_entity, { :primary_name => inner_xml, :rules => att('rules'), :authority_id => att('authfilenumber'), :source => att('source') || 'ingest' } do |name| set ancestor(:agent_corporate_entity), :names, proxy end end def make_family_template(opts) return nil if inner_xml.strip.empty? make :agent_family, { :agent_type => 'agent_family', :publish => att('audience') == 'external' ? true : false } do |family| set ancestor(:resource, :archival_object), :linked_agents, {'ref' => family.uri, 'role' => opts[:role], 'relator' => att('role')} end make :name_family, { :family_name => inner_xml, :rules => att('rules'), :authority_id => att('authfilenumber'), :source => att('source') || 'ingest' } do |name| set ancestor(:agent_family), :names, name end end def make_person_template(opts) return nil if inner_xml.strip.empty? make :agent_person, { :agent_type => 'agent_person', :publish => att('audience') == 'external' ? true : false } do |person| set ancestor(:resource, :archival_object), :linked_agents, {'ref' => person.uri, 'role' => opts[:role], 'relator' => att('role')} end make :name_person, { :name_order => 'inverted', :primary_name => inner_xml, :authority_id => att('authfilenumber'), :rules => att('rules'), :source => att('source') || 'ingest' } do |name| set ancestor(:agent_person), :names, name end end end