neu-mods 0.3.0 → 0.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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/README.md +14 -0
- data/lib/neu/mods/canonicalize.rb +30 -7
- data/lib/neu/mods/projection.rb +18 -3
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89680abe072d9ece72ed12e15a8301aca55c7a8a3ec5f6ee9fb092ebd507eeac
|
|
4
|
+
data.tar.gz: d65dddefd46aa15b4408cd1ba952faf24d603edfb59b5b1b556e46ea982875d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 882dd1f0cd4f57a90d55c1c06ba67e3f5595e68fe8908eb40069d1dbf9215e72685ddf48c832ebe48b510aac1063cfe2ed251c7dc5dd06130b56fd357c432237
|
|
7
|
+
data.tar.gz: fcb62c4fe88773b8224938a35fc254a7c0eb22845e3084f257b3fa1ed8c49b95da7aa6c81d7531868cfc0d772db594bd2098884dce8f4a4bc98ea37358f359d7
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
data/README.md
CHANGED
|
@@ -29,6 +29,7 @@ doc = NEU::MODS::Document.parse(xml_string)
|
|
|
29
29
|
# Projection (plain data)
|
|
30
30
|
doc.plain_title # => "What's New - How We Respond to Disaster, Episode 1"
|
|
31
31
|
doc.title_parts # => { non_sort:, subtitle:, title:, part_name:, part_number: }
|
|
32
|
+
# byte-faithful -- the edit forms pre-fill from these
|
|
32
33
|
doc.abstract # => normalized, paragraph-joined String
|
|
33
34
|
doc.topical_subjects # => ["Civil society", ...] (every <topic>, for the access copy)
|
|
34
35
|
doc.keywords # => [...] (only the editable attribute-free keyword subjects)
|
|
@@ -70,6 +71,19 @@ keyword-subject curated-vs-editable split. `build_*_name`'s `role:` defaults to
|
|
|
70
71
|
the JSON/Solr access copy (dash/smart-punctuation transliteration, control
|
|
71
72
|
stripping, paragraph handling). The XML preservation copy is never touched.
|
|
72
73
|
|
|
74
|
+
Titles and prose share the one freetext vocabulary: `to_h[:main_title]` is
|
|
75
|
+
normalized like `abstract`, so an invisible format mark, a Windows-1252 control
|
|
76
|
+
or an exotic space cannot reach Solr or a display template.
|
|
77
|
+
|
|
78
|
+
**The boundary matters.** Normalization belongs on projections that only feed
|
|
79
|
+
display and the index. `title_parts` is deliberately *not* normalized, because
|
|
80
|
+
Cerberus pre-fills its Metadata and Advanced forms from it and `MODSMerge` writes
|
|
81
|
+
the posted value back into the MODS XML — cleaning there would rewrite the
|
|
82
|
+
curator's own characters in the preservation copy on the next save. Cerberus
|
|
83
|
+
makes the same call for prose: its editable source is the bare `<abstract>` node,
|
|
84
|
+
not `doc.abstract`. Add a normalized *sibling* rather than normalizing a
|
|
85
|
+
projection an edit form reads.
|
|
86
|
+
|
|
73
87
|
## Behavior fidelity & known caveats
|
|
74
88
|
|
|
75
89
|
The projection is **behavior-preserving** with Atlas's prior `mods`-gem-based
|
|
@@ -37,9 +37,11 @@ module NEU
|
|
|
37
37
|
#
|
|
38
38
|
# Pipeline: force UTF-8 + scrub invalid bytes; NFC; map Unicode dashes to '-'
|
|
39
39
|
# (swung-dash to '~'); transliterate the General Punctuation block (smart
|
|
40
|
-
# quotes, ellipsis, etc.) to ASCII;
|
|
41
|
-
#
|
|
42
|
-
#
|
|
40
|
+
# quotes, ellipsis, etc.) to ASCII; map the separator controls to a newline
|
|
41
|
+
# and drop what is invisible (the soft hyphen, the rest of C0/C1, keeping
|
|
42
|
+
# tab/newline); collapse
|
|
43
|
+
# horizontal-whitespace runs to one space; for paragraph fields, collapse
|
|
44
|
+
# 2+ newlines to exactly two; strip.
|
|
43
45
|
#
|
|
44
46
|
# .normalize(str) -- single-line fields (newlines -> spaces)
|
|
45
47
|
# .normalize_paragraphs(str) -- fields that may carry paragraph breaks
|
|
@@ -56,7 +58,7 @@ module NEU
|
|
|
56
58
|
# NOTE: U+2053 (swung dash) is intentionally excluded from dashes -- it is
|
|
57
59
|
# named "dash" but conventionally maps to ASCII '~', not '-' (V1 prior art).
|
|
58
60
|
DASH_CODEPOINTS = [
|
|
59
|
-
0x002D,
|
|
61
|
+
0x002D, 0x058A, 0x05BE, 0x1400, 0x1806,
|
|
60
62
|
0x2010, 0x2011, 0x2012, 0x2013, 0x2014, 0x2015,
|
|
61
63
|
0x2043, 0x207B, 0x208B, 0x2212,
|
|
62
64
|
0x2E17, 0x2E1A, 0x2E3A, 0x2E3B, 0x2E40,
|
|
@@ -67,9 +69,28 @@ module NEU
|
|
|
67
69
|
|
|
68
70
|
SWUNG_DASH_RE = Regexp.new(format('\\u%04X', 0x2053)).freeze
|
|
69
71
|
|
|
70
|
-
#
|
|
71
|
-
# and
|
|
72
|
-
|
|
72
|
+
# U+00AD is a hint about where a word may break, not a dash: it renders as
|
|
73
|
+
# nothing, and Solr discards it, so "co<00AD>operation" already matches a
|
|
74
|
+
# search for "cooperation". Mapping it to an ASCII hyphen instead would
|
|
75
|
+
# index "co" and "operation" as two tokens and lose the word, so it is
|
|
76
|
+
# dropped and kept out of DASH_CODEPOINTS.
|
|
77
|
+
SOFT_HYPHEN_RE = Regexp.new(format('\\u%04X', 0x00AD)).freeze
|
|
78
|
+
|
|
79
|
+
# U+000B (vertical tab) and U+000C (form feed) separate words rather than
|
|
80
|
+
# meaning nothing: Word writes a manual line break as U+000B and a page
|
|
81
|
+
# break as U+000C. They map to a newline, because deleting one runs the
|
|
82
|
+
# words either side of it together -- normalize_paragraphs then reads that
|
|
83
|
+
# newline as the soft wrap the line break was, and normalize turns it into
|
|
84
|
+
# a space.
|
|
85
|
+
SEPARATOR_CONTROL_CODEPOINTS = [0x000B, 0x000C].freeze
|
|
86
|
+
SEPARATOR_CONTROL_RE = char_class(SEPARATOR_CONTROL_CODEPOINTS).freeze
|
|
87
|
+
|
|
88
|
+
# C0 (U+0000..U+0008, U+000D..U+001F) and C1 (U+007F..U+009F) -- what is
|
|
89
|
+
# left once the separators above are accounted for, and none of it carries
|
|
90
|
+
# meaning in curator text. U+0009 (tab) and U+000A (newline) are preserved.
|
|
91
|
+
# U+000D is not: dropping it reduces a CRLF line ending to the single
|
|
92
|
+
# newline it stands for.
|
|
93
|
+
CONTROL_CODEPOINTS = ((0x0000..0x0008).to_a + (0x000D..0x001F).to_a + (0x007F..0x009F).to_a).freeze
|
|
73
94
|
CONTROL_RE = char_class(CONTROL_CODEPOINTS).freeze
|
|
74
95
|
|
|
75
96
|
HORIZONTAL_WS_CODEPOINTS = [
|
|
@@ -136,6 +157,8 @@ module NEU
|
|
|
136
157
|
s = s.gsub(DASH_RE, "-")
|
|
137
158
|
s = s.gsub(SWUNG_DASH_RE, "~")
|
|
138
159
|
s = s.gsub(GENERAL_PUNCTUATION_RE) { |c| GENERAL_PUNCTUATION.fetch(c, c) }
|
|
160
|
+
s = s.gsub(SEPARATOR_CONTROL_RE, "\n")
|
|
161
|
+
s = s.gsub(SOFT_HYPHEN_RE, "")
|
|
139
162
|
s.gsub(CONTROL_RE, "")
|
|
140
163
|
end
|
|
141
164
|
end
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -15,8 +15,15 @@ module NEU
|
|
|
15
15
|
module Projection
|
|
16
16
|
# --- Title ---------------------------------------------------------------
|
|
17
17
|
|
|
18
|
-
# Structured primary-title parts. nil for an
|
|
19
|
-
# treats nil as "not present"); to_h coerces
|
|
18
|
+
# Structured primary-title parts, byte-faithful to the document. nil for an
|
|
19
|
+
# absent part (the Cerberus form treats nil as "not present"); to_h coerces
|
|
20
|
+
# to "" for the Atlas main_title.
|
|
21
|
+
#
|
|
22
|
+
# Faithful on purpose: this is what Cerberus pre-fills its edit forms from
|
|
23
|
+
# (MODSFields for the Metadata tab, load_advanced! for the Advanced tab),
|
|
24
|
+
# and MODSMerge writes back whatever the form posts. Normalising here would
|
|
25
|
+
# rewrite the curator's characters in the preservation XML on the next save.
|
|
26
|
+
# #access_title_parts is the normalised surface.
|
|
20
27
|
def title_parts
|
|
21
28
|
ti = primary_title_info
|
|
22
29
|
{
|
|
@@ -186,7 +193,7 @@ module NEU
|
|
|
186
193
|
# names -- a drop-in source for `convert_xml_to_json`.
|
|
187
194
|
def to_h
|
|
188
195
|
{
|
|
189
|
-
main_title:
|
|
196
|
+
main_title: access_title_parts,
|
|
190
197
|
names: names,
|
|
191
198
|
languages: languages,
|
|
192
199
|
date_created: date_created,
|
|
@@ -204,6 +211,14 @@ module NEU
|
|
|
204
211
|
}
|
|
205
212
|
end
|
|
206
213
|
|
|
214
|
+
# The title parts as the access copy wants them: normalised like the
|
|
215
|
+
# abstract, so a curly quote, an invisible format mark or a Windows-1252
|
|
216
|
+
# control cannot reach Solr or a display template. Titles and prose share
|
|
217
|
+
# one vocabulary -- the asymmetry where only prose was cleaned was the bug.
|
|
218
|
+
def access_title_parts
|
|
219
|
+
title_parts.transform_values { |value| NEU::MODS.normalize(value.to_s) }
|
|
220
|
+
end
|
|
221
|
+
|
|
207
222
|
private
|
|
208
223
|
|
|
209
224
|
# --- helpers -------------------------------------------------------------
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: neu-mods
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -92,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
93
|
version: '0'
|
|
94
94
|
requirements: []
|
|
95
|
-
rubygems_version: 3.
|
|
95
|
+
rubygems_version: 3.2.33
|
|
96
96
|
signing_key:
|
|
97
97
|
specification_version: 4
|
|
98
98
|
summary: Northeastern-flavored MODS XML projection + selection for the DRS.
|