arclight 0.0.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. checksums.yaml +5 -5
  2. data/.all-contributorsrc +450 -0
  3. data/.codeclimate.yml +5 -0
  4. data/.eslintrc +22 -0
  5. data/.github/workflows/ruby.yml +51 -0
  6. data/.gitignore +7 -0
  7. data/.rspec +0 -1
  8. data/.rubocop.yml +521 -0
  9. data/.rubocop_todo.yml +122 -0
  10. data/.solr_wrapper +6 -0
  11. data/CONTRIBUTING.md +43 -0
  12. data/CONTRIBUTORS.md +79 -0
  13. data/Gemfile +41 -0
  14. data/LICENSE.txt +1 -0
  15. data/README.md +100 -12
  16. data/Rakefile +13 -3
  17. data/app/assets/images/arclight/logo.png +0 -0
  18. data/app/assets/images/blacklight/bookmark.svg +1 -0
  19. data/app/assets/images/blacklight/collection.svg +5 -0
  20. data/app/assets/images/blacklight/compact.svg +1 -0
  21. data/app/assets/images/blacklight/container.svg +5 -0
  22. data/app/assets/images/blacklight/ead.svg +1 -0
  23. data/app/assets/images/blacklight/file.svg +5 -0
  24. data/app/assets/images/blacklight/folder.svg +1 -0
  25. data/app/assets/images/blacklight/list.svg +1 -0
  26. data/app/assets/images/blacklight/minus.svg +1 -0
  27. data/app/assets/images/blacklight/online.svg +5 -0
  28. data/app/assets/images/blacklight/pdf.svg +1 -0
  29. data/app/assets/images/blacklight/plus.svg +1 -0
  30. data/app/assets/images/blacklight/repository.svg +1 -0
  31. data/app/assets/javascripts/arclight/arclight.js +4 -0
  32. data/app/assets/javascripts/arclight/oembed_controller.js +58 -0
  33. data/app/assets/javascripts/arclight/truncate_controller.js +27 -0
  34. data/app/assets/stylesheets/arclight/application.scss +14 -0
  35. data/app/assets/stylesheets/arclight/build.scss +4 -0
  36. data/app/assets/stylesheets/arclight/modules/collection_search.scss +3 -0
  37. data/app/assets/stylesheets/arclight/modules/hierarchy_and_online_contents.scss +155 -0
  38. data/app/assets/stylesheets/arclight/modules/highlights.scss +11 -0
  39. data/app/assets/stylesheets/arclight/modules/icons.scss +20 -0
  40. data/app/assets/stylesheets/arclight/modules/layout.scss +220 -0
  41. data/app/assets/stylesheets/arclight/modules/mastheads.scss +42 -0
  42. data/app/assets/stylesheets/arclight/modules/repositories.scss +25 -0
  43. data/app/assets/stylesheets/arclight/modules/repository_card.scss +9 -0
  44. data/app/assets/stylesheets/arclight/modules/search_form.scss +9 -0
  45. data/app/assets/stylesheets/arclight/modules/search_results.scss +250 -0
  46. data/app/assets/stylesheets/arclight/modules/show_collection.scss +38 -0
  47. data/app/assets/stylesheets/arclight/modules/truncator.scss +58 -0
  48. data/app/assets/stylesheets/arclight/responsive.scss +13 -0
  49. data/app/assets/stylesheets/arclight/variables.scss +25 -0
  50. data/app/components/arclight/access_component.html.erb +14 -0
  51. data/app/components/arclight/access_component.rb +25 -0
  52. data/app/components/arclight/bookmark_component.html.erb +25 -0
  53. data/app/components/arclight/bookmark_component.rb +9 -0
  54. data/app/components/arclight/breadcrumb_component.rb +50 -0
  55. data/app/components/arclight/breadcrumbs_hierarchy_component.html.erb +34 -0
  56. data/app/components/arclight/breadcrumbs_hierarchy_component.rb +25 -0
  57. data/app/components/arclight/collection_context_component.html.erb +12 -0
  58. data/app/components/arclight/collection_context_component.rb +27 -0
  59. data/app/components/arclight/collection_info_component.html.erb +28 -0
  60. data/app/components/arclight/collection_info_component.rb +26 -0
  61. data/app/components/arclight/collection_sidebar_component.html.erb +15 -0
  62. data/app/components/arclight/collection_sidebar_component.rb +39 -0
  63. data/app/components/arclight/constraints_component.html.erb +7 -0
  64. data/app/components/arclight/constraints_component.rb +17 -0
  65. data/app/components/arclight/document_collection_context_component.html.erb +29 -0
  66. data/app/components/arclight/document_collection_context_component.rb +28 -0
  67. data/app/components/arclight/document_collection_hierarchy_component.html.erb +50 -0
  68. data/app/components/arclight/document_collection_hierarchy_component.rb +62 -0
  69. data/app/components/arclight/document_component.html.erb +39 -0
  70. data/app/components/arclight/document_component.rb +49 -0
  71. data/app/components/arclight/document_components_hierarchy_component.html.erb +20 -0
  72. data/app/components/arclight/document_components_hierarchy_component.rb +34 -0
  73. data/app/components/arclight/document_download_component.html.erb +24 -0
  74. data/app/components/arclight/document_download_component.rb +71 -0
  75. data/app/components/arclight/embed_component.html.erb +10 -0
  76. data/app/components/arclight/embed_component.rb +43 -0
  77. data/app/components/arclight/expand_hierarchy_button_component.html.erb +5 -0
  78. data/app/components/arclight/expand_hierarchy_button_component.rb +16 -0
  79. data/app/components/arclight/group_component.html.erb +34 -0
  80. data/app/components/arclight/group_component.rb +23 -0
  81. data/app/components/arclight/header_component.html.erb +5 -0
  82. data/app/components/arclight/header_component.rb +10 -0
  83. data/app/components/arclight/index_metadata_field_component.html.erb +16 -0
  84. data/app/components/arclight/index_metadata_field_component.rb +20 -0
  85. data/app/components/arclight/masthead_component.html.erb +17 -0
  86. data/app/components/arclight/masthead_component.rb +10 -0
  87. data/app/components/arclight/metadata_section_component.html.erb +13 -0
  88. data/app/components/arclight/metadata_section_component.rb +22 -0
  89. data/app/components/arclight/oembed_viewer_component.html.erb +5 -0
  90. data/app/components/arclight/oembed_viewer_component.rb +16 -0
  91. data/app/components/arclight/online_content_filter_component.html.erb +15 -0
  92. data/app/components/arclight/online_content_filter_component.rb +17 -0
  93. data/app/components/arclight/online_status_indicator_component.rb +19 -0
  94. data/app/components/arclight/repository_breadcrumb_component.html.erb +10 -0
  95. data/app/components/arclight/repository_breadcrumb_component.rb +17 -0
  96. data/app/components/arclight/repository_location_component.html.erb +20 -0
  97. data/app/components/arclight/repository_location_component.rb +10 -0
  98. data/app/components/arclight/search_bar_component.html.erb +27 -0
  99. data/app/components/arclight/search_bar_component.rb +32 -0
  100. data/app/components/arclight/search_result_breadcrumbs_component.html.erb +6 -0
  101. data/app/components/arclight/search_result_breadcrumbs_component.rb +37 -0
  102. data/app/components/arclight/search_result_component.html.erb +20 -0
  103. data/app/components/arclight/search_result_component.rb +18 -0
  104. data/app/components/arclight/search_result_title_component.html.erb +15 -0
  105. data/app/components/arclight/search_result_title_component.rb +15 -0
  106. data/app/components/arclight/sidebar_component.html.erb +9 -0
  107. data/app/components/arclight/sidebar_component.rb +19 -0
  108. data/app/components/arclight/upper_metadata_layout_component.rb +10 -0
  109. data/app/components/blacklight/icons/bookmark_component.rb +12 -0
  110. data/app/components/blacklight/icons/collection_component.rb +14 -0
  111. data/app/components/blacklight/icons/compact_component.rb +12 -0
  112. data/app/components/blacklight/icons/container_component.rb +14 -0
  113. data/app/components/blacklight/icons/ead_component.rb +12 -0
  114. data/app/components/blacklight/icons/file_component.rb +14 -0
  115. data/app/components/blacklight/icons/folder_component.rb +12 -0
  116. data/app/components/blacklight/icons/online_component.rb +15 -0
  117. data/app/components/blacklight/icons/pdf_component.rb +12 -0
  118. data/app/components/blacklight/icons/repository_component.rb +12 -0
  119. data/app/controllers/arclight/repositories_controller.rb +40 -0
  120. data/app/helpers/arclight/ead_format_helpers.rb +309 -0
  121. data/app/helpers/arclight/field_config_helpers.rb +26 -0
  122. data/app/helpers/arclight_helper.rb +108 -0
  123. data/app/models/arclight/document_downloads.rb +122 -0
  124. data/app/models/arclight/parent.rb +24 -0
  125. data/app/models/arclight/parents.rb +39 -0
  126. data/app/models/arclight/requests/aeon_external_request.rb +42 -0
  127. data/app/models/arclight/requests/aeon_web_ead.rb +52 -0
  128. data/app/models/arclight/requests/google_form.rb +49 -0
  129. data/app/models/concerns/arclight/catalog.rb +44 -0
  130. data/app/models/concerns/arclight/search_behavior.rb +47 -0
  131. data/app/models/concerns/arclight/solr_document.rb +160 -0
  132. data/app/presenters/arclight/index_presenter.rb +10 -0
  133. data/app/presenters/arclight/show_presenter.rb +54 -0
  134. data/app/views/arclight/.keep +0 -0
  135. data/app/views/arclight/_requests.html.erb +7 -0
  136. data/app/views/arclight/repositories/_repository.html.erb +43 -0
  137. data/app/views/arclight/repositories/index.html.erb +9 -0
  138. data/app/views/arclight/repositories/show.html.erb +37 -0
  139. data/app/views/arclight/requests/_aeon_external_request_endpoint.html.erb +8 -0
  140. data/app/views/arclight/requests/_aeon_web_ead.html.erb +6 -0
  141. data/app/views/arclight/requests/_google_form.html.erb +10 -0
  142. data/app/views/catalog/_document_list.html.erb +8 -0
  143. data/app/views/catalog/_group.html.erb +4 -0
  144. data/app/views/catalog/_group_toggle.html.erb +10 -0
  145. data/app/views/catalog/_search_results_header.html.erb +3 -0
  146. data/app/views/catalog/hierarchy.html.erb +19 -0
  147. data/app/views/catalog/index.html.erb +17 -0
  148. data/app/views/shared/_breadcrumbs.html.erb +3 -0
  149. data/app/views/shared/_main_menu_links.html.erb +6 -0
  150. data/arclight.gemspec +29 -11
  151. data/bin/rails +13 -0
  152. data/config/breadcrumbs.rb +24 -0
  153. data/config/i18n-tasks.yml +134 -0
  154. data/config/importmap.rb +3 -0
  155. data/config/locales/arclight.en.yml +100 -0
  156. data/config/repositories.yml +0 -0
  157. data/config/routes.rb +6 -0
  158. data/docker-compose.yml +16 -0
  159. data/lib/arclight/digital_object.rb +27 -0
  160. data/lib/arclight/engine.rb +47 -0
  161. data/lib/arclight/exceptions.rb +19 -0
  162. data/lib/arclight/hash_absolute_xpath.rb +62 -0
  163. data/lib/arclight/level_label.rb +46 -0
  164. data/lib/arclight/missing_id_strategy.rb +21 -0
  165. data/lib/arclight/normalized_date.rb +49 -0
  166. data/lib/arclight/normalized_id.rb +30 -0
  167. data/lib/arclight/normalized_title.rb +33 -0
  168. data/lib/arclight/repository.rb +116 -0
  169. data/lib/arclight/routes/hierarchy.rb +19 -0
  170. data/lib/arclight/routes.rb +8 -0
  171. data/lib/arclight/traject/ead2_component_config.rb +335 -0
  172. data/lib/arclight/traject/ead2_config.rb +316 -0
  173. data/lib/arclight/traject/nokogiri_namespaceless_reader.rb +22 -0
  174. data/lib/arclight/version.rb +3 -1
  175. data/lib/arclight/year_range.rb +111 -0
  176. data/lib/arclight.rb +11 -1
  177. data/lib/generators/arclight/install_generator.rb +163 -0
  178. data/lib/generators/arclight/templates/arclight.scss +6 -0
  179. data/lib/generators/arclight/templates/catalog_controller.rb +423 -0
  180. data/lib/generators/arclight/templates/config/downloads.yml +25 -0
  181. data/lib/generators/arclight/templates/config/locales/arclight.en.yml +61 -0
  182. data/lib/generators/arclight/templates/config/repositories.yml +53 -0
  183. data/lib/generators/arclight/update_generator.rb +22 -0
  184. data/lib/tasks/index.rake +84 -0
  185. data/package.json +30 -0
  186. data/solr/conf/_rest_managed.json +3 -0
  187. data/solr/conf/admin-extra.html +31 -0
  188. data/solr/conf/elevate.xml +36 -0
  189. data/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  190. data/solr/conf/protwords.txt +21 -0
  191. data/solr/conf/schema.xml +400 -0
  192. data/solr/conf/solrconfig.xml +328 -0
  193. data/solr/conf/spellings.txt +2 -0
  194. data/solr/conf/stopwords.txt +58 -0
  195. data/solr/conf/stopwords_en.txt +58 -0
  196. data/solr/conf/synonyms.txt +31 -0
  197. data/solr/conf/xslt/example.xsl +132 -0
  198. data/solr/conf/xslt/example_atom.xsl +67 -0
  199. data/solr/conf/xslt/example_rss.xsl +66 -0
  200. data/solr/conf/xslt/luke.xsl +337 -0
  201. data/tasks/arclight.rake +79 -0
  202. data/template.rb +10 -0
  203. metadata +440 -20
  204. data/.travis.yml +0 -5
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arclight/exceptions'
4
+
5
+ module Arclight
6
+ ##
7
+ # A utility class to normalize titles, typically by joining
8
+ # the title and date, e.g., "My Title, 1990-2000"
9
+ class NormalizedTitle
10
+ # @param [String] `title` from the `unittitle`
11
+ # @param [String] `date` from the `unitdate`
12
+ def initialize(title, date = nil)
13
+ @title = title.gsub(/\s*,\s*$/, '').strip if title.present?
14
+ @date = date.strip if date.present?
15
+ end
16
+
17
+ # @return [String] the normalized title/date
18
+ def to_s
19
+ normalize
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :title, :date, :default
25
+
26
+ def normalize
27
+ result = [title, date].compact.join(', ')
28
+ raise Arclight::Exceptions::TitleNotFound if result.blank?
29
+
30
+ result
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+
7
+ module Arclight
8
+ # Static information about a given repository identified by a unique `slug`.
9
+ # These data are loaded from config/repositories.yml
10
+ class Repository
11
+ include ActiveModel::Model
12
+ DEFAULTS = {
13
+ request_types: {},
14
+ contact_html: '',
15
+ location_html: '',
16
+ visit_note: nil
17
+ }.freeze
18
+
19
+ def initialize(attributes = {})
20
+ @attributes = DEFAULTS.merge(attributes).with_indifferent_access
21
+ end
22
+
23
+ attr_reader :attributes
24
+ attr_accessor :collection_count
25
+
26
+ def method_missing(field, *args, &block)
27
+ return attributes[field] if attributes.include?(field)
28
+
29
+ super
30
+ end
31
+
32
+ def respond_to_missing?(field, *args)
33
+ attributes.include?(field) || super
34
+ end
35
+
36
+ # rubocop:disable Rails/OutputSafety
37
+ def contact
38
+ contact_html.html_safe
39
+ end
40
+
41
+ def location
42
+ location_html.html_safe
43
+ end
44
+ # rubocop:enable Rails/OutputSafety
45
+
46
+ def request_config_present?
47
+ request_configs = request_types.values || []
48
+ request_configs.dig(0, 'request_url').present? &&
49
+ request_configs.dig(0, 'request_mappings').present?
50
+ end
51
+
52
+ def request_config_present_for_type?(type)
53
+ config = request_config_for_type(type)
54
+
55
+ config['request_url'].present? &&
56
+ config['request_mappings'].present?
57
+ end
58
+
59
+ def request_config_for_type(type)
60
+ request_types.fetch(type, {})
61
+ end
62
+
63
+ def available_request_types
64
+ return [] unless request_types.present?
65
+
66
+ request_types.keys
67
+ end
68
+
69
+ # Load repository information from a YAML file
70
+ #
71
+ # @param [String] `filename`
72
+ # @return [Hash<Slug,Repository>]
73
+ def self.from_yaml(file)
74
+ repos = {}
75
+ data = YAML.safe_load_file(file)
76
+ data.each_key do |slug|
77
+ repos[slug] = new(data[slug].merge(slug: slug))
78
+ end
79
+ repos
80
+ end
81
+
82
+ # Mimics ActiveRecord's `all` behavior
83
+ #
84
+ # @return [Array<Repository>]
85
+ def self.all(yaml_file = nil)
86
+ yaml_file = ENV['REPOSITORY_FILE'] || 'config/repositories.yml' if yaml_file.nil?
87
+ from_yaml(yaml_file).values
88
+ end
89
+
90
+ # Mimics ActiveRecord dynamic `find_by` behavior for the slug or name
91
+ #
92
+ # @param [String] `slug` or `name`
93
+ # @return [Repository]
94
+ def self.find_by(slug: nil, name: nil, yaml_file: nil)
95
+ if slug
96
+ all(yaml_file).find { |repo| repo.slug == slug }
97
+ elsif name
98
+ all(yaml_file).find { |repo| repo.name == name }
99
+ else
100
+ raise ArgumentError, 'Requires either slug or name parameters to find_by'
101
+ end
102
+ end
103
+
104
+ # Mimics ActiveRecord dynamic `find_by!` behavior for the slug or name
105
+ #
106
+ # @param [String] `slug` or `name` -- same as `find_by`
107
+ # @return [Repository]
108
+ # @raise [ActiveRecord::RecordNotFound] if cannot find repository
109
+ def self.find_by!(**kwargs)
110
+ repository = find_by(**kwargs)
111
+ raise ActiveRecord::RecordNotFound if repository.blank?
112
+
113
+ repository
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ module Routes
5
+ # Inject a hierarchy route for displaying the
6
+ # components in the collection context
7
+ class Hierarchy
8
+ def initialize(defaults = {})
9
+ @defaults = defaults
10
+ end
11
+
12
+ def call(mapper, _options = {})
13
+ mapper.member do
14
+ mapper.get 'hierarchy'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ # Custom routes for Arclight
5
+ module Routes
6
+ require 'arclight/routes/hierarchy'
7
+ end
8
+ end
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'traject'
5
+ require 'traject/nokogiri_reader'
6
+ require 'traject_plus'
7
+ require 'traject_plus/macros'
8
+ require 'arclight/level_label'
9
+ require 'arclight/normalized_date'
10
+ require 'arclight/normalized_title'
11
+ require 'active_model/conversion' ## Needed for Arclight::Repository
12
+ require 'active_support/core_ext/array/wrap'
13
+ require 'arclight/digital_object'
14
+ require 'arclight/year_range'
15
+ require 'arclight/missing_id_strategy'
16
+ require 'arclight/traject/nokogiri_namespaceless_reader'
17
+
18
+ # rubocop:disable Style/MixinUsage
19
+ extend TrajectPlus::Macros
20
+ # rubocop:enable Style/MixinUsage
21
+
22
+ settings do
23
+ # provide 'root' # the root EAD collection indexing context
24
+ # provide 'parent' # the immediate parent component (or collection) indexing context
25
+ # provide 'counter' # a global component counter to provide a global sort order for nested components
26
+ # provide 'depth' # the current nesting depth of the component
27
+ provide 'component_traject_config', __FILE__
28
+ provide 'date_normalizer', 'Arclight::NormalizedDate'
29
+ provide 'title_normalizer', 'Arclight::NormalizedTitle'
30
+ provide 'reader_class_name', 'Arclight::Traject::NokogiriNamespacelessReader'
31
+ provide 'logger', Logger.new($stderr)
32
+ provide 'component_identifier_format', '%<root_id>s_%<ref_id>s'
33
+ end
34
+
35
+ NAME_ELEMENTS = %w[corpname famname name persname].freeze
36
+
37
+ SEARCHABLE_NOTES_FIELDS = %w[
38
+ accessrestrict
39
+ accruals
40
+ altformavail
41
+ appraisal
42
+ arrangement
43
+ bibliography
44
+ bioghist
45
+ custodhist
46
+ fileplan
47
+ note
48
+ odd
49
+ originalsloc
50
+ otherfindaid
51
+ phystech
52
+ prefercite
53
+ processinfo
54
+ relatedmaterial
55
+ scopecontent
56
+ separatedmaterial
57
+ userestrict
58
+ ].freeze
59
+
60
+ DID_SEARCHABLE_NOTES_FIELDS = %w[
61
+ abstract
62
+ materialspec
63
+ physloc
64
+ note
65
+ ].freeze
66
+
67
+ # ==================
68
+ # Component elements
69
+ #
70
+ # NOTE: All fields should be stored in Solr
71
+ # ==================
72
+ to_field 'ref_ssi' do |record, accumulator, context|
73
+ next if context.output_hash['ref_ssi']
74
+
75
+ accumulator << if record.attribute('id').blank?
76
+ strategy = Arclight::MissingIdStrategy.selected
77
+ hexdigest = strategy.new(record).to_hexdigest
78
+ parent_id = settings[:parent].output_hash['id'].first
79
+ root_id = settings[:root].output_hash['id'].first
80
+ logger.warn('MISSING ID WARNING') do
81
+ [
82
+ "A component in #{parent_id} did not have an ID so one was minted using the #{strategy} strategy.",
83
+ "The ID of this document will be #{root_id}#{hexdigest}."
84
+ ].join(' ')
85
+ end
86
+ record['id'] = hexdigest
87
+ hexdigest
88
+ else
89
+ record.attribute('id')&.value&.strip&.gsub('.', '-') # rubocop:disable Style/SafeNavigationChainLength
90
+ end
91
+ end
92
+ to_field 'ref_ssm' do |_record, accumulator, context|
93
+ accumulator.concat context.output_hash['ref_ssi']
94
+ end
95
+
96
+ to_field 'id' do |_record, accumulator, context|
97
+ next if context.output_hash['id']
98
+
99
+ data = {
100
+ root_id: settings[:root].output_hash['id']&.first,
101
+ ref_id: context.output_hash['ref_ssi']&.first
102
+ }
103
+
104
+ accumulator << (settings[:component_identifier_format] % data)
105
+ end
106
+
107
+ to_field 'title_filing_ssi', extract_xpath('./did/unittitle'), first_only
108
+ to_field 'title_ssm', extract_xpath('./did/unittitle')
109
+ to_field 'title_tesim', extract_xpath('./did/unittitle')
110
+
111
+ to_field 'unitdate_bulk_ssim', extract_xpath('./did/unitdate[@type="bulk"]')
112
+ to_field 'unitdate_inclusive_ssm', extract_xpath('./did/unitdate[@type="inclusive"]')
113
+ to_field 'unitdate_other_ssim', extract_xpath('./did/unitdate[not(@type)]')
114
+
115
+ to_field 'normalized_date_ssm' do |_record, accumulator, context|
116
+ accumulator << settings['date_normalizer'].constantize.new(
117
+ context.output_hash['unitdate_inclusive_ssm'],
118
+ context.output_hash['unitdate_bulk_ssim'],
119
+ context.output_hash['unitdate_other_ssim']
120
+ ).to_s
121
+ end
122
+
123
+ to_field 'normalized_title_ssm' do |_record, accumulator, context|
124
+ title = context.output_hash['title_ssm']&.first
125
+ date = context.output_hash['normalized_date_ssm']&.first
126
+ accumulator << settings['title_normalizer'].constantize.new(title, date).to_s
127
+ end
128
+
129
+ to_field 'component_level_isim' do |_record, accumulator|
130
+ accumulator << (settings[:depth] || 1)
131
+ end
132
+
133
+ to_field 'parent_ssim' do |_record, accumulator, _context|
134
+ accumulator.concat(settings[:parent].output_hash['parent_ssim'] || [])
135
+ accumulator.concat settings[:parent].output_hash['ref_ssi'] || settings[:parent].output_hash['id']
136
+ end
137
+
138
+ to_field 'parent_ssi' do |_record, accumulator, _context|
139
+ accumulator.concat settings[:parent].output_hash['ref_ssi'] || settings[:parent].output_hash['id']
140
+ end
141
+
142
+ to_field 'parent_ids_ssim' do |_record, accumulator, _context|
143
+ accumulator.concat(settings[:parent].output_hash['parent_ids_ssim'] || [])
144
+ accumulator.concat settings[:parent].output_hash['id']
145
+ end
146
+
147
+ to_field 'parent_unittitles_ssm' do |_rec, accumulator, _context|
148
+ accumulator.concat(settings[:parent].output_hash['parent_unittitles_ssm'] || [])
149
+ accumulator.concat settings[:parent].output_hash['normalized_title_ssm'] || []
150
+ end
151
+
152
+ to_field 'parent_unittitles_tesim' do |_record, accumulator, context|
153
+ accumulator.concat context.output_hash['parent_unittitles_ssm']
154
+ end
155
+
156
+ to_field 'parent_levels_ssm' do |_record, accumulator, _context|
157
+ ## Top level document
158
+ accumulator.concat settings[:parent].output_hash['parent_levels_ssm'] || []
159
+ accumulator.concat settings[:parent].output_hash['level_ssm'] || []
160
+ end
161
+
162
+ to_field 'unitid_ssm', extract_xpath('./did/unitid')
163
+ to_field 'repository_ssim' do |_record, accumulator, _context|
164
+ accumulator << settings[:root].clipboard[:repository]
165
+ end
166
+
167
+ to_field 'collection_ssim' do |_record, accumulator, _context|
168
+ accumulator.concat settings[:root].output_hash['normalized_title_ssm']
169
+ end
170
+
171
+ # This accumulates direct text from a physdesc, ignoring child elements handled elsewhere
172
+ to_field 'physdesc_tesim', extract_xpath('./did/physdesc', to_text: false) do |_record, accumulator|
173
+ accumulator.map! do |element|
174
+ physdesc = []
175
+ element.children.map do |child|
176
+ next if child.instance_of?(Nokogiri::XML::Element)
177
+
178
+ physdesc << child.text&.strip unless child.text&.strip&.empty?
179
+ end.flatten
180
+ physdesc.join(' ') unless physdesc.empty?
181
+ end
182
+ end
183
+
184
+ to_field 'extent_ssm' do |record, accumulator|
185
+ physdescs = record.xpath('./did/physdesc')
186
+ extents_per_physdesc = physdescs.map do |physdesc|
187
+ extents = physdesc.xpath('./extent').map { |e| e.text.strip }
188
+ # Join extents within the same physdesc with an empty string
189
+ extents.join(' ') unless extents.empty?
190
+ end
191
+
192
+ # Add each physdesc separately to the accumulator
193
+ accumulator.concat(extents_per_physdesc)
194
+ end
195
+
196
+ to_field 'extent_tesim' do |_record, accumulator, context|
197
+ accumulator.concat context.output_hash['extent_ssm'] || []
198
+ end
199
+
200
+ to_field 'physfacet_tesim', extract_xpath('./did/physdesc/physfacet')
201
+ to_field 'dimensions_tesim', extract_xpath('./did/physdesc/dimensions')
202
+
203
+ to_field 'indexes_html_tesm', extract_xpath('./index', to_text: false)
204
+ to_field 'indexes_tesim', extract_xpath('./index')
205
+
206
+ to_field 'creator_ssim', extract_xpath('./did/origination')
207
+ to_field 'creator_sort' do |record, accumulator|
208
+ accumulator << record.xpath('./did/origination').map(&:text).join(', ')
209
+ end
210
+ to_field 'has_online_content_ssim', extract_xpath('.//dao') do |_record, accumulator|
211
+ accumulator.replace([accumulator.any?])
212
+ end
213
+ to_field 'child_component_count_isi' do |record, accumulator|
214
+ accumulator << record.xpath('c|c01|c02|c03|c04|c05|c06|c07|c08|c09|c10|c11|c12').count
215
+ end
216
+
217
+ to_field 'ref_ssm' do |record, accumulator|
218
+ accumulator << record.attribute('id')
219
+ end
220
+
221
+ to_field 'level_ssm' do |record, accumulator|
222
+ level = record.attribute('level')&.value
223
+ other_level = record.attribute('otherlevel')&.value
224
+ accumulator << Arclight::LevelLabel.new(level, other_level).to_s
225
+ end
226
+
227
+ to_field 'level_ssim' do |_record, accumulator, context|
228
+ next unless context.output_hash['level_ssm']
229
+
230
+ accumulator.concat context.output_hash['level_ssm']&.map(&:capitalize)
231
+ end
232
+
233
+ to_field 'sort_isi' do |_record, accumulator, _context|
234
+ accumulator.replace([settings[:counter].increment])
235
+ end
236
+
237
+ # Get the <accessrestrict> from the closest ancestor that has one (includes top-level)
238
+ to_field 'parent_access_restrict_tesm' do |record, accumulator|
239
+ accumulator.concat Array
240
+ .wrap(record.xpath('(./ancestor::*/accessrestrict)[last()]/*[local-name()!="head"]')
241
+ .map(&:text))
242
+ end
243
+
244
+ # Get the <userestrict> from self OR the closest ancestor that has one (includes top-level)
245
+ to_field 'parent_access_terms_tesm' do |record, accumulator|
246
+ accumulator.concat Array
247
+ .wrap(record.xpath('(./ancestor-or-self::*/userestrict)[last()]/*[local-name()!="head"]')
248
+ .map(&:text))
249
+ end
250
+
251
+ to_field 'digital_objects_ssm', extract_xpath('./dao|./did/dao', to_text: false) do |_record, accumulator|
252
+ accumulator.map! do |dao|
253
+ label = dao.attributes['title']&.value ||
254
+ dao.xpath('daodesc/p')&.text
255
+ href = (dao.attributes['href'] || dao.attributes['xlink:href'])&.value
256
+ Arclight::DigitalObject.new(label: label, href: href).to_json
257
+ end
258
+ end
259
+
260
+ to_field 'date_range_isim', extract_xpath('./did/unitdate/@normal', to_text: false) do |_record, accumulator|
261
+ range = Arclight::YearRange.new
262
+ next range.years if accumulator.blank?
263
+
264
+ ranges = accumulator.map(&:to_s)
265
+ range << range.parse_ranges(ranges)
266
+ accumulator.replace range.years
267
+ end
268
+
269
+ NAME_ELEMENTS.map do |selector|
270
+ to_field 'names_ssim', extract_xpath("./controlaccess/#{selector}"), unique
271
+ to_field 'names_ssim', extract_xpath(".//#{selector}"), unique
272
+ to_field "#{selector}_ssim", extract_xpath(".//#{selector}")
273
+ end
274
+
275
+ to_field 'geogname_ssim', extract_xpath('./controlaccess/geogname')
276
+ to_field 'geogname_ssm', extract_xpath('./controlaccess/geogname')
277
+ to_field 'places_ssim', extract_xpath('./controlaccess/geogname')
278
+
279
+ to_field 'access_subjects_ssim', extract_xpath('./controlaccess', to_text: false) do |_record, accumulator|
280
+ accumulator.map! do |element|
281
+ %w[subject function occupation genreform].map do |selector|
282
+ element.xpath(".//#{selector}").map(&:text)
283
+ end
284
+ end.flatten!
285
+ end
286
+
287
+ to_field 'access_subjects_ssm' do |_record, accumulator, context|
288
+ accumulator.concat(context.output_hash.fetch('access_subjects_ssim', []))
289
+ end
290
+
291
+ to_field 'acqinfo_ssim', extract_xpath('./acqinfo/*[local-name()!="head"]')
292
+ to_field 'acqinfo_ssim', extract_xpath('./descgrp/acqinfo/*[local-name()!="head"]')
293
+
294
+ to_field 'language_ssim', extract_xpath('./did/langmaterial')
295
+ to_field 'containers_ssim' do |record, accumulator|
296
+ record.xpath('./did/container').each do |node|
297
+ accumulator << [node.attribute('type'), node.text].join(' ').strip
298
+ end
299
+ end
300
+
301
+ SEARCHABLE_NOTES_FIELDS.map do |selector|
302
+ to_field "#{selector}_html_tesm", extract_xpath("./#{selector}/*[local-name()!='head']", to_text: false)
303
+ to_field "#{selector}_heading_ssm", extract_xpath("./#{selector}/head")
304
+ to_field "#{selector}_tesim", extract_xpath("./#{selector}/*[local-name()!='head']")
305
+ end
306
+ DID_SEARCHABLE_NOTES_FIELDS.map do |selector|
307
+ to_field "#{selector}_html_tesm", extract_xpath("./did/#{selector}", to_text: false)
308
+ to_field "#{selector}_tesim", extract_xpath("./did/#{selector}")
309
+ end
310
+
311
+ # =============================
312
+ # Each component child document
313
+ # <c> <c01> <c12>
314
+ # =============================
315
+
316
+ to_field 'components' do |record, accumulator, context|
317
+ child_components = record.xpath('c|c01|c02|c03|c04|c05|c06|c07|c08|c09|c10|c11|c12')
318
+ component_indexer = Traject::Indexer::NokogiriIndexer.new.tap do |i|
319
+ i.settings do
320
+ provide :parent, context
321
+ provide :root, context.settings[:root]
322
+ provide :counter, context.settings[:counter]
323
+ provide :depth, context.settings[:depth].to_i + 1
324
+ provide :component_traject_config, context.settings[:component_traject_config]
325
+ provide :component_identifier_format, context.settings[:component_identifier_format]
326
+ end
327
+
328
+ i.load_config_file(context.settings[:component_traject_config])
329
+ end
330
+
331
+ child_components.each do |child_component|
332
+ output = component_indexer.map_record(child_component)
333
+ accumulator << output if output.keys.any?
334
+ end
335
+ end