arclight 0.0.1 → 1.4.0

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 (202) 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 +44 -0
  6. data/.gitignore +7 -0
  7. data/.rspec +0 -1
  8. data/.rubocop.yml +362 -0
  9. data/.rubocop_todo.yml +122 -0
  10. data/.solr_wrapper +5 -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 +57 -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 +40 -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 +32 -0
  72. data/app/components/arclight/document_components_hierarchy_component.rb +28 -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/group_component.html.erb +34 -0
  78. data/app/components/arclight/group_component.rb +23 -0
  79. data/app/components/arclight/header_component.html.erb +5 -0
  80. data/app/components/arclight/header_component.rb +10 -0
  81. data/app/components/arclight/index_metadata_field_component.html.erb +16 -0
  82. data/app/components/arclight/index_metadata_field_component.rb +20 -0
  83. data/app/components/arclight/masthead_component.html.erb +15 -0
  84. data/app/components/arclight/masthead_component.rb +10 -0
  85. data/app/components/arclight/metadata_section_component.html.erb +13 -0
  86. data/app/components/arclight/metadata_section_component.rb +22 -0
  87. data/app/components/arclight/oembed_viewer_component.html.erb +5 -0
  88. data/app/components/arclight/oembed_viewer_component.rb +16 -0
  89. data/app/components/arclight/online_content_filter_component.html.erb +15 -0
  90. data/app/components/arclight/online_content_filter_component.rb +19 -0
  91. data/app/components/arclight/online_status_indicator_component.rb +19 -0
  92. data/app/components/arclight/repository_breadcrumb_component.html.erb +10 -0
  93. data/app/components/arclight/repository_breadcrumb_component.rb +17 -0
  94. data/app/components/arclight/repository_location_component.html.erb +20 -0
  95. data/app/components/arclight/repository_location_component.rb +10 -0
  96. data/app/components/arclight/search_bar_component.html.erb +27 -0
  97. data/app/components/arclight/search_bar_component.rb +32 -0
  98. data/app/components/arclight/search_result_breadcrumbs_component.html.erb +6 -0
  99. data/app/components/arclight/search_result_breadcrumbs_component.rb +37 -0
  100. data/app/components/arclight/search_result_component.html.erb +20 -0
  101. data/app/components/arclight/search_result_component.rb +18 -0
  102. data/app/components/arclight/search_result_title_component.html.erb +15 -0
  103. data/app/components/arclight/search_result_title_component.rb +15 -0
  104. data/app/components/arclight/sidebar_component.html.erb +9 -0
  105. data/app/components/arclight/sidebar_component.rb +19 -0
  106. data/app/components/arclight/upper_metadata_layout_component.rb +10 -0
  107. data/app/components/blacklight/icons/bookmark_component.rb +12 -0
  108. data/app/components/blacklight/icons/collection_component.rb +14 -0
  109. data/app/components/blacklight/icons/compact_component.rb +12 -0
  110. data/app/components/blacklight/icons/container_component.rb +14 -0
  111. data/app/components/blacklight/icons/ead_component.rb +12 -0
  112. data/app/components/blacklight/icons/file_component.rb +14 -0
  113. data/app/components/blacklight/icons/folder_component.rb +12 -0
  114. data/app/components/blacklight/icons/online_component.rb +15 -0
  115. data/app/components/blacklight/icons/pdf_component.rb +12 -0
  116. data/app/components/blacklight/icons/repository_component.rb +12 -0
  117. data/app/controllers/arclight/repositories_controller.rb +40 -0
  118. data/app/helpers/arclight/ead_format_helpers.rb +309 -0
  119. data/app/helpers/arclight/field_config_helpers.rb +26 -0
  120. data/app/helpers/arclight_helper.rb +108 -0
  121. data/app/models/arclight/document_downloads.rb +122 -0
  122. data/app/models/arclight/parent.rb +24 -0
  123. data/app/models/arclight/parents.rb +39 -0
  124. data/app/models/arclight/requests/aeon_external_request.rb +42 -0
  125. data/app/models/arclight/requests/aeon_web_ead.rb +52 -0
  126. data/app/models/arclight/requests/google_form.rb +49 -0
  127. data/app/models/concerns/arclight/catalog.rb +44 -0
  128. data/app/models/concerns/arclight/search_behavior.rb +47 -0
  129. data/app/models/concerns/arclight/solr_document.rb +159 -0
  130. data/app/presenters/arclight/index_presenter.rb +10 -0
  131. data/app/presenters/arclight/show_presenter.rb +54 -0
  132. data/app/views/arclight/.keep +0 -0
  133. data/app/views/arclight/_requests.html.erb +7 -0
  134. data/app/views/arclight/repositories/_repository.html.erb +43 -0
  135. data/app/views/arclight/repositories/index.html.erb +9 -0
  136. data/app/views/arclight/repositories/show.html.erb +37 -0
  137. data/app/views/arclight/requests/_aeon_external_request_endpoint.html.erb +8 -0
  138. data/app/views/arclight/requests/_aeon_web_ead.html.erb +6 -0
  139. data/app/views/arclight/requests/_google_form.html.erb +10 -0
  140. data/app/views/catalog/_document_list.html.erb +8 -0
  141. data/app/views/catalog/_group.html.erb +4 -0
  142. data/app/views/catalog/_group_toggle.html.erb +10 -0
  143. data/app/views/catalog/_search_results_header.html.erb +3 -0
  144. data/app/views/catalog/hierarchy.html.erb +19 -0
  145. data/app/views/catalog/index.html.erb +17 -0
  146. data/app/views/shared/_breadcrumbs.html.erb +3 -0
  147. data/app/views/shared/_main_menu_links.html.erb +6 -0
  148. data/arclight.gemspec +29 -11
  149. data/bin/rails +13 -0
  150. data/config/breadcrumbs.rb +24 -0
  151. data/config/i18n-tasks.yml +134 -0
  152. data/config/importmap.rb +3 -0
  153. data/config/locales/arclight.en.yml +100 -0
  154. data/config/repositories.yml +0 -0
  155. data/config/routes.rb +6 -0
  156. data/docker-compose.yml +16 -0
  157. data/lib/arclight/digital_object.rb +27 -0
  158. data/lib/arclight/engine.rb +47 -0
  159. data/lib/arclight/exceptions.rb +19 -0
  160. data/lib/arclight/hash_absolute_xpath.rb +62 -0
  161. data/lib/arclight/level_label.rb +46 -0
  162. data/lib/arclight/missing_id_strategy.rb +21 -0
  163. data/lib/arclight/normalized_date.rb +49 -0
  164. data/lib/arclight/normalized_id.rb +30 -0
  165. data/lib/arclight/normalized_title.rb +33 -0
  166. data/lib/arclight/repository.rb +116 -0
  167. data/lib/arclight/routes/hierarchy.rb +19 -0
  168. data/lib/arclight/routes.rb +8 -0
  169. data/lib/arclight/traject/ead2_component_config.rb +334 -0
  170. data/lib/arclight/traject/ead2_config.rb +315 -0
  171. data/lib/arclight/traject/nokogiri_namespaceless_reader.rb +22 -0
  172. data/lib/arclight/version.rb +3 -1
  173. data/lib/arclight/year_range.rb +111 -0
  174. data/lib/arclight.rb +11 -1
  175. data/lib/generators/arclight/install_generator.rb +126 -0
  176. data/lib/generators/arclight/templates/arclight.scss +5 -0
  177. data/lib/generators/arclight/templates/catalog_controller.rb +422 -0
  178. data/lib/generators/arclight/templates/config/downloads.yml +25 -0
  179. data/lib/generators/arclight/templates/config/locales/arclight.en.yml +61 -0
  180. data/lib/generators/arclight/templates/config/repositories.yml +53 -0
  181. data/lib/generators/arclight/update_generator.rb +22 -0
  182. data/lib/tasks/index.rake +84 -0
  183. data/package.json +30 -0
  184. data/solr/conf/_rest_managed.json +3 -0
  185. data/solr/conf/admin-extra.html +31 -0
  186. data/solr/conf/elevate.xml +36 -0
  187. data/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  188. data/solr/conf/protwords.txt +21 -0
  189. data/solr/conf/schema.xml +400 -0
  190. data/solr/conf/solrconfig.xml +328 -0
  191. data/solr/conf/spellings.txt +2 -0
  192. data/solr/conf/stopwords.txt +58 -0
  193. data/solr/conf/stopwords_en.txt +58 -0
  194. data/solr/conf/synonyms.txt +31 -0
  195. data/solr/conf/xslt/example.xsl +132 -0
  196. data/solr/conf/xslt/example_atom.xsl +67 -0
  197. data/solr/conf/xslt/example_rss.xsl +66 -0
  198. data/solr/conf/xslt/luke.xsl +337 -0
  199. data/tasks/arclight.rake +72 -0
  200. data/template.rb +10 -0
  201. metadata +432 -20
  202. data/.travis.yml +0 -5
@@ -0,0 +1,334 @@
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('.', '-')
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 "#{selector}_ssim", extract_xpath(".//#{selector}")
272
+ end
273
+
274
+ to_field 'geogname_ssim', extract_xpath('./controlaccess/geogname')
275
+ to_field 'geogname_ssm', extract_xpath('./controlaccess/geogname')
276
+ to_field 'places_ssim', extract_xpath('./controlaccess/geogname')
277
+
278
+ to_field 'access_subjects_ssim', extract_xpath('./controlaccess', to_text: false) do |_record, accumulator|
279
+ accumulator.map! do |element|
280
+ %w[subject function occupation genreform].map do |selector|
281
+ element.xpath(".//#{selector}").map(&:text)
282
+ end
283
+ end.flatten!
284
+ end
285
+
286
+ to_field 'access_subjects_ssm' do |_record, accumulator, context|
287
+ accumulator.concat(context.output_hash.fetch('access_subjects_ssim', []))
288
+ end
289
+
290
+ to_field 'acqinfo_ssim', extract_xpath('./acqinfo/*[local-name()!="head"]')
291
+ to_field 'acqinfo_ssim', extract_xpath('./descgrp/acqinfo/*[local-name()!="head"]')
292
+
293
+ to_field 'language_ssim', extract_xpath('./did/langmaterial')
294
+ to_field 'containers_ssim' do |record, accumulator|
295
+ record.xpath('./did/container').each do |node|
296
+ accumulator << [node.attribute('type'), node.text].join(' ').strip
297
+ end
298
+ end
299
+
300
+ SEARCHABLE_NOTES_FIELDS.map do |selector|
301
+ to_field "#{selector}_html_tesm", extract_xpath("./#{selector}/*[local-name()!='head']", to_text: false)
302
+ to_field "#{selector}_heading_ssm", extract_xpath("./#{selector}/head")
303
+ to_field "#{selector}_tesim", extract_xpath("./#{selector}/*[local-name()!='head']")
304
+ end
305
+ DID_SEARCHABLE_NOTES_FIELDS.map do |selector|
306
+ to_field "#{selector}_html_tesm", extract_xpath("./did/#{selector}", to_text: false)
307
+ to_field "#{selector}_tesim", extract_xpath("./did/#{selector}")
308
+ end
309
+
310
+ # =============================
311
+ # Each component child document
312
+ # <c> <c01> <c12>
313
+ # =============================
314
+
315
+ to_field 'components' do |record, accumulator, context|
316
+ child_components = record.xpath('c|c01|c02|c03|c04|c05|c06|c07|c08|c09|c10|c11|c12')
317
+ component_indexer = Traject::Indexer::NokogiriIndexer.new.tap do |i|
318
+ i.settings do
319
+ provide :parent, context
320
+ provide :root, context.settings[:root]
321
+ provide :counter, context.settings[:counter]
322
+ provide :depth, context.settings[:depth].to_i + 1
323
+ provide :component_traject_config, context.settings[:component_traject_config]
324
+ provide :component_identifier_format, context.settings[:component_identifier_format]
325
+ end
326
+
327
+ i.load_config_file(context.settings[:component_traject_config])
328
+ end
329
+
330
+ child_components.each do |child_component|
331
+ output = component_indexer.map_record(child_component)
332
+ accumulator << output if output.keys.any?
333
+ end
334
+ end
@@ -0,0 +1,315 @@
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_id'
10
+ require 'arclight/normalized_date'
11
+ require 'arclight/normalized_title'
12
+ require 'active_model/conversion' ## Needed for Arclight::Repository
13
+ require 'active_support/core_ext/array/wrap'
14
+ require 'arclight/digital_object'
15
+ require 'arclight/year_range'
16
+ require 'arclight/repository'
17
+ require 'arclight/traject/nokogiri_namespaceless_reader'
18
+
19
+ # rubocop:disable Style/MixinUsage
20
+ extend TrajectPlus::Macros
21
+ # rubocop:enable Style/MixinUsage
22
+
23
+ NAME_ELEMENTS = %w[corpname famname name persname].freeze
24
+
25
+ SEARCHABLE_NOTES_FIELDS = %w[
26
+ accessrestrict
27
+ accruals
28
+ altformavail
29
+ appraisal
30
+ arrangement
31
+ bibliography
32
+ bioghist
33
+ custodhist
34
+ fileplan
35
+ note
36
+ odd
37
+ originalsloc
38
+ otherfindaid
39
+ phystech
40
+ prefercite
41
+ processinfo
42
+ relatedmaterial
43
+ scopecontent
44
+ separatedmaterial
45
+ userestrict
46
+ ].freeze
47
+
48
+ DID_SEARCHABLE_NOTES_FIELDS = %w[
49
+ abstract
50
+ materialspec
51
+ physloc
52
+ note
53
+ ].freeze
54
+
55
+ settings do
56
+ provide 'component_traject_config', File.join(__dir__, 'ead2_component_config.rb')
57
+ provide 'id_normalizer', 'Arclight::NormalizedId'
58
+ provide 'date_normalizer', 'Arclight::NormalizedDate'
59
+ provide 'title_normalizer', 'Arclight::NormalizedTitle'
60
+ provide 'reader_class_name', 'Arclight::Traject::NokogiriNamespacelessReader'
61
+ provide 'solr_writer.commit_on_close', 'true'
62
+ provide 'repository', ENV.fetch('REPOSITORY_ID', nil)
63
+ provide 'logger', Logger.new($stderr)
64
+ end
65
+
66
+ each_record do |_record, context|
67
+ next unless settings['repository']
68
+
69
+ context.clipboard[:repository] = Arclight::Repository.find_by(
70
+ slug: settings['repository']
71
+ ).name
72
+ end
73
+
74
+ # ==================
75
+ # Top level document
76
+ #
77
+ # NOTE: All fields should be stored in Solr
78
+ # ==================
79
+
80
+ to_field 'id' do |record, accumulator|
81
+ id = record.at_xpath('/ead/eadheader/eadid')&.text
82
+ title = record.at_xpath('/ead/archdesc/did/unittitle')&.text
83
+ repository = settings['repository']
84
+ accumulator << settings['id_normalizer'].constantize.new(id, title: title, repository: repository).to_s
85
+ end
86
+
87
+ to_field 'title_filing_ssi', extract_xpath('/ead/eadheader/filedesc/titlestmt/titleproper[@type="filing"]')
88
+ to_field 'title_ssm', extract_xpath('/ead/archdesc/did/unittitle')
89
+ to_field 'title_tesim', extract_xpath('/ead/archdesc/did/unittitle')
90
+ to_field 'ead_ssi', extract_xpath('/ead/eadheader/eadid')
91
+
92
+ to_field 'unitdate_ssm', extract_xpath('/ead/archdesc/did/unitdate')
93
+ to_field 'unitdate_bulk_ssim', extract_xpath('/ead/archdesc/did/unitdate[@type="bulk"]')
94
+ to_field 'unitdate_inclusive_ssm', extract_xpath('/ead/archdesc/did/unitdate[@type="inclusive"]')
95
+ to_field 'unitdate_other_ssim', extract_xpath('/ead/archdesc/did/unitdate[not(@type)]')
96
+
97
+ # All top-level docs treated as 'collection' for routing / display purposes
98
+ to_field 'level_ssm' do |_record, accumulator|
99
+ accumulator << 'collection'
100
+ end
101
+
102
+ # Keep the original top-level archdesc/@level for Level facet in addition to 'Collection'
103
+ to_field 'level_ssim' do |record, accumulator|
104
+ level = record.at_xpath('/ead/archdesc').attribute('level')&.value
105
+ other_level = record.at_xpath('/ead/archdesc').attribute('otherlevel')&.value
106
+
107
+ accumulator << Arclight::LevelLabel.new(level, other_level).to_s
108
+ accumulator << 'Collection' unless level == 'collection'
109
+ end
110
+
111
+ to_field 'unitid_ssm', extract_xpath('/ead/archdesc/did/unitid')
112
+ to_field 'unitid_tesim', extract_xpath('/ead/archdesc/did/unitid')
113
+
114
+ to_field 'normalized_date_ssm' do |_record, accumulator, context|
115
+ accumulator << settings['date_normalizer'].constantize.new(
116
+ context.output_hash['unitdate_inclusive_ssm'],
117
+ context.output_hash['unitdate_bulk_ssim'],
118
+ context.output_hash['unitdate_other_ssim']
119
+ ).to_s
120
+ end
121
+
122
+ to_field 'normalized_title_ssm' do |_record, accumulator, context|
123
+ title = context.output_hash['title_ssm']&.first
124
+ date = context.output_hash['normalized_date_ssm']&.first
125
+ accumulator << settings['title_normalizer'].constantize.new(title, date).to_s
126
+ end
127
+
128
+ to_field 'collection_title_tesim' do |_record, accumulator, context|
129
+ accumulator.concat context.output_hash.fetch('normalized_title_ssm', [])
130
+ end
131
+
132
+ to_field 'collection_ssim' do |_record, accumulator, context|
133
+ accumulator.concat context.output_hash.fetch('normalized_title_ssm', [])
134
+ end
135
+
136
+ to_field 'repository_ssm' do |_record, accumulator, context|
137
+ accumulator << context.clipboard[:repository]
138
+ end
139
+
140
+ to_field 'repository_ssim' do |_record, accumulator, context|
141
+ accumulator << context.clipboard[:repository]
142
+ end
143
+
144
+ to_field 'geogname_ssm', extract_xpath('/ead/archdesc/controlaccess/geogname')
145
+ to_field 'geogname_ssim', extract_xpath('/ead/archdesc/controlaccess/geogname')
146
+
147
+ to_field 'creator_ssm', extract_xpath('/ead/archdesc/did/origination')
148
+ to_field 'creator_ssim', extract_xpath('/ead/archdesc/did/origination')
149
+ to_field 'creator_sort' do |record, accumulator|
150
+ accumulator << record.xpath('/ead/archdesc/did/origination').map { |c| c.text.strip }.join(', ')
151
+ end
152
+
153
+ to_field 'creator_persname_ssim', extract_xpath('/ead/archdesc/did/origination/persname')
154
+ to_field 'creator_corpname_ssim', extract_xpath('/ead/archdesc/did/origination/corpname')
155
+ to_field 'creator_famname_ssim', extract_xpath('/ead/archdesc/did/origination/famname')
156
+
157
+ to_field 'creators_ssim' do |_record, accumulator, context|
158
+ accumulator.concat context.output_hash['creator_persname_ssim'] if context.output_hash['creator_persname_ssim']
159
+ accumulator.concat context.output_hash['creator_corpname_ssim'] if context.output_hash['creator_corpname_ssim']
160
+ accumulator.concat context.output_hash['creator_famname_ssim'] if context.output_hash['creator_famname_ssim']
161
+ end
162
+
163
+ to_field 'places_ssim', extract_xpath('/ead/archdesc/controlaccess/geogname')
164
+
165
+ to_field 'access_terms_ssm', extract_xpath('/ead/archdesc/userestrict/*[local-name()!="head"]')
166
+
167
+ to_field 'acqinfo_ssim', extract_xpath('/ead/archdesc/acqinfo/*[local-name()!="head"]')
168
+ to_field 'acqinfo_ssim', extract_xpath('/ead/archdesc/descgrp/acqinfo/*[local-name()!="head"]')
169
+
170
+ to_field 'access_subjects_ssim', extract_xpath('/ead/archdesc/controlaccess', to_text: false) do |_record, accumulator|
171
+ accumulator.map! do |element|
172
+ %w[subject function occupation genreform].map do |selector|
173
+ element.xpath(".//#{selector}").map(&:text)
174
+ end
175
+ end.flatten!
176
+ end
177
+
178
+ to_field 'access_subjects_ssm' do |_record, accumulator, context|
179
+ accumulator.concat Array.wrap(context.output_hash['access_subjects_ssim'])
180
+ end
181
+
182
+ to_field 'has_online_content_ssim', extract_xpath('.//dao') do |_record, accumulator|
183
+ accumulator.replace([accumulator.any?])
184
+ end
185
+
186
+ to_field 'digital_objects_ssm', extract_xpath('/ead/archdesc/did/dao|/ead/archdesc/dao', to_text: false) do |_record, accumulator|
187
+ accumulator.map! do |dao|
188
+ label = dao.attributes['title']&.value ||
189
+ dao.xpath('daodesc/p')&.text
190
+ href = (dao.attributes['href'] || dao.attributes['xlink:href'])&.value
191
+ Arclight::DigitalObject.new(label: label, href: href).to_json
192
+ end
193
+ end
194
+
195
+ # This accumulates direct text from a physdesc, ignoring child elements handled elsewhere
196
+ to_field 'physdesc_tesim', extract_xpath('/ead/archdesc/did/physdesc', to_text: false) do |_record, accumulator|
197
+ accumulator.map! do |element|
198
+ physdesc = []
199
+ element.children.map do |child|
200
+ next if child.instance_of?(Nokogiri::XML::Element)
201
+
202
+ physdesc << child.text&.strip unless child.text&.strip&.empty?
203
+ end.flatten
204
+ physdesc.join(' ') unless physdesc.empty?
205
+ end
206
+ end
207
+
208
+ to_field 'extent_ssm' do |record, accumulator|
209
+ physdescs = record.xpath('/ead/archdesc/did/physdesc')
210
+ extents_per_physdesc = physdescs.map do |physdesc|
211
+ extents = physdesc.xpath('./extent').map { |e| e.text.strip }
212
+ # Join extents within the same physdesc with an empty string
213
+ extents.join(' ') unless extents.empty?
214
+ end
215
+
216
+ # Add each physdesc separately to the accumulator
217
+ accumulator.concat(extents_per_physdesc)
218
+ end
219
+
220
+ to_field 'extent_tesim' do |_record, accumulator, context|
221
+ accumulator.concat context.output_hash['extent_ssm'] || []
222
+ end
223
+
224
+ to_field 'physfacet_tesim', extract_xpath('/ead/archdesc/did/physdesc/physfacet')
225
+ to_field 'dimensions_tesim', extract_xpath('/ead/archdesc/did/physdesc/dimensions')
226
+
227
+ to_field 'genreform_ssim', extract_xpath('/ead/archdesc/controlaccess/genreform')
228
+
229
+ to_field 'date_range_isim', extract_xpath('/ead/archdesc/did/unitdate/@normal', to_text: false) do |_record, accumulator|
230
+ range = Arclight::YearRange.new
231
+ next range.years if accumulator.blank?
232
+
233
+ ranges = accumulator.map(&:to_s)
234
+ range << range.parse_ranges(ranges)
235
+ accumulator.replace range.years
236
+ end
237
+
238
+ to_field 'indexes_html_tesm', extract_xpath('/ead/archdesc/index', to_text: false)
239
+ to_field 'indexes_tesim', extract_xpath('/ead/archdesc/index')
240
+
241
+ SEARCHABLE_NOTES_FIELDS.map do |selector|
242
+ to_field "#{selector}_html_tesm", extract_xpath("/ead/archdesc/#{selector}/*[local-name()!='head']", to_text: false) do |_record, accumulator|
243
+ accumulator.map!(&:to_html)
244
+ end
245
+ to_field "#{selector}_heading_ssm", extract_xpath("/ead/archdesc/#{selector}/head") unless selector == 'prefercite'
246
+ to_field "#{selector}_tesim", extract_xpath("/ead/archdesc/#{selector}/*[local-name()!='head']")
247
+ end
248
+
249
+ DID_SEARCHABLE_NOTES_FIELDS.map do |selector|
250
+ to_field "#{selector}_html_tesm", extract_xpath("/ead/archdesc/did/#{selector}", to_text: false)
251
+ to_field "#{selector}_tesim", extract_xpath("/ead/archdesc/did/#{selector}")
252
+ end
253
+
254
+ NAME_ELEMENTS.map do |selector|
255
+ to_field 'names_coll_ssim', extract_xpath("/ead/archdesc/controlaccess/#{selector}")
256
+ to_field 'names_ssim', extract_xpath("//#{selector}"), unique
257
+ to_field "#{selector}_ssim", extract_xpath("//#{selector}"), unique
258
+ end
259
+
260
+ to_field 'language_ssim', extract_xpath('/ead/archdesc/did/langmaterial')
261
+
262
+ to_field 'descrules_ssm', extract_xpath('/ead/eadheader/profiledesc/descrules')
263
+
264
+ # count all descendant components from the top-level
265
+ to_field 'total_component_count_is', first_only do |record, accumulator|
266
+ accumulator << record.xpath('//c|//c01|//c02|//c03|//c04|//c05|//c06|//c07|//c08|//c09|//c10|//c11|//c12').count
267
+ end
268
+
269
+ # count all digital objects from the top-level
270
+ to_field 'online_item_count_is', first_only do |record, accumulator|
271
+ accumulator << record.xpath('//dao').count
272
+ end
273
+
274
+ to_field 'component_level_isim' do |_record, accumulator, _context|
275
+ accumulator << 0
276
+ end
277
+
278
+ to_field 'sort_isi' do |_record, accumulator, _context|
279
+ accumulator << 0
280
+ end
281
+
282
+ # =============================
283
+ # Each component child document
284
+ # <c> <c01> <c12>
285
+ # =============================
286
+
287
+ to_field 'components' do |record, accumulator, context|
288
+ child_components = record.xpath("/ead/archdesc/dsc/c|#{('/ead/archdesc/dsc/c01'..'/ead/archdesc/dsc/c12').to_a.join('|')}")
289
+ next unless child_components.any?
290
+
291
+ counter = Class.new do
292
+ def increment
293
+ @counter ||= 0
294
+ @counter += 1
295
+ end
296
+ end.new
297
+
298
+ component_indexer = Traject::Indexer::NokogiriIndexer.new.tap do |i|
299
+ i.settings do
300
+ provide :parent, context
301
+ provide :root, context
302
+ provide :counter, counter
303
+ provide :depth, 1
304
+ provide :logger, context.settings[:logger]
305
+ provide :component_traject_config, context.settings[:component_traject_config]
306
+ end
307
+
308
+ i.load_config_file(context.settings[:component_traject_config])
309
+ end
310
+
311
+ child_components.each do |child_component|
312
+ output = component_indexer.map_record(child_component)
313
+ accumulator << output if output.keys.any?
314
+ end
315
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ module Traject
5
+ # Provides a Traject Reader for XML Documents which removes the namespaces
6
+ class NokogiriNamespacelessReader < ::Traject::NokogiriReader
7
+ # Overrides the #each method (which is used for iterating through each Document)
8
+ # @param args
9
+ # @see ::Traject::NokogiriReader#each
10
+ # @see Enumerable#each
11
+ def each(*args)
12
+ return to_enum(:each, *args) unless block_given?
13
+
14
+ super do |doc|
15
+ new_doc = doc.dup
16
+ new_doc.remove_namespaces!
17
+ yield new_doc
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Arclight
2
- VERSION = '0.0.1'
4
+ VERSION = '1.4.0'
3
5
  end