relaton-doi 2.1.1 → 2.1.3
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/CLAUDE.md +6 -0
- data/lib/relaton/doi/parser.rb +87 -12
- data/lib/relaton/doi/version.rb +1 -1
- data/relaton-doi.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 953aa1acd54cd1a21fb180ba132e24da1a1f9bb310496bfc4bc454e6d8d73621
|
|
4
|
+
data.tar.gz: 3a60c3a21e9eb9b1b32bc62a57bdea14f8efbad2beddf490648dbc073c63cf7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3061d50b8b594725c9ae4ca531fe593ccda1d9825a51ea6fce7b046367684920e068470fbb8b95f3ef29a76469f3b5f9281938bf9adffadd573c046bfc9a526b
|
|
7
|
+
data.tar.gz: 4a04be0cdde3781c793db668665c5a4d2f66530af19b505c646a9bb535a9276ab9984b09efee690aad0ff79a32ec128f6f48b5021cfabed7ac204783e18b5ec2
|
data/CLAUDE.md
CHANGED
|
@@ -43,3 +43,9 @@ Key classes in `lib/relaton/doi/`:
|
|
|
43
43
|
- `TYPES` — maps 23 Crossref document types to Relaton types (e.g., `"book-chapter"` → `"inbook"`)
|
|
44
44
|
- `REALATION_TYPES` — maps 37 Crossref relation types to Relaton relation types
|
|
45
45
|
- `COUNTRIES` — `%w[USA]`, used by `parse_place` to distinguish country vs region
|
|
46
|
+
- `TAG_RE`, `NS_PREFIX_RE`, `NS_PLACEHOLDER`, `SAVE_OPTS` — used by `normalize_markup` and
|
|
47
|
+
`drop_namespaces` to remove the `jats:` and `xlink:` prefixes that Crossref puts on
|
|
48
|
+
abstract and title markup. Relaton never declares those prefixes, so the output would
|
|
49
|
+
otherwise fail every namespace-aware parser downstream, including relaton-render
|
|
50
|
+
(metanorma-pdfa#99). The helper declares each prefix on a wrapper element, removes the
|
|
51
|
+
namespaces with Nokogiri, and returns the content unchanged when it does not parse.
|
data/lib/relaton/doi/parser.rb
CHANGED
|
@@ -73,6 +73,22 @@ module Relaton
|
|
|
73
73
|
ATTRS = %i[type fetched title docidentifier date source abstract contributor place
|
|
74
74
|
ext relation extent series medium].freeze
|
|
75
75
|
|
|
76
|
+
# Matches content that looks like markup. Same shape as the tag detector
|
|
77
|
+
# of `Relaton::Bib::Sanitizer`, so both agree on what to parse.
|
|
78
|
+
TAG_RE = %r{<[a-zA-Z/!?]}
|
|
79
|
+
|
|
80
|
+
# Captures a namespace prefix, on a tag or on an attribute: the `jats` of
|
|
81
|
+
# `<jats:p>` and `</jats:italic>`, and the `xlink` of `xlink:href`.
|
|
82
|
+
NS_PREFIX_RE = %r{(?:</?|\s)([A-Za-z_][\w.-]*):(?=[A-Za-z_])}
|
|
83
|
+
|
|
84
|
+
# Placeholder namespace, used only to make an undeclared prefix parseable.
|
|
85
|
+
NS_PLACEHOLDER = "urn:x-relaton-doi:%s".freeze
|
|
86
|
+
|
|
87
|
+
# Serialize without the FORMAT option, so the round trip through the
|
|
88
|
+
# parser does not add indentation to the content.
|
|
89
|
+
SAVE_OPTS = Nokogiri::XML::Node::SaveOptions::AS_XML |
|
|
90
|
+
Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
|
|
91
|
+
|
|
76
92
|
CROSSREF_API_URL = "https://api.crossref.org/works?query=%{query}&filter=%{filter}".freeze
|
|
77
93
|
MAX_RETRIES = 3
|
|
78
94
|
|
|
@@ -209,10 +225,69 @@ module Relaton
|
|
|
209
225
|
# @return [Bib::Title] The title.
|
|
210
226
|
#
|
|
211
227
|
def create_title(title, type = "main")
|
|
212
|
-
cnt = str_cleanup title
|
|
228
|
+
cnt = str_cleanup normalize_markup(title)
|
|
213
229
|
Bib::Title.new type: type, content: cnt, script: "Latn"
|
|
214
230
|
end
|
|
215
231
|
|
|
232
|
+
#
|
|
233
|
+
# Prepare raw Crossref markup for the relaton-bib sanitizer.
|
|
234
|
+
#
|
|
235
|
+
# Crossref returns JATS markup. It sometimes encodes the markup as HTML
|
|
236
|
+
# entities, and it prefixes the elements with the `jats:` namespace
|
|
237
|
+
# prefix. The prefix is never declared in the Relaton output, so a
|
|
238
|
+
# namespace-aware parser rejects the document, and the relaton-bib
|
|
239
|
+
# sanitizer skips content that it cannot parse. Decode the entities and
|
|
240
|
+
# remove the prefixes, so the sanitizer maps the elements to the
|
|
241
|
+
# basicdoc set. See metanorma-pdfa#99.
|
|
242
|
+
#
|
|
243
|
+
# @param [String] str The raw Crossref content.
|
|
244
|
+
#
|
|
245
|
+
# @return [String] The content without entities and namespace prefixes.
|
|
246
|
+
#
|
|
247
|
+
def normalize_markup(str)
|
|
248
|
+
cnt = CGI.unescapeHTML(str)
|
|
249
|
+
cnt.match?(TAG_RE) ? drop_namespaces(cnt) : cnt
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
#
|
|
253
|
+
# Remove the namespace prefixes from markup.
|
|
254
|
+
#
|
|
255
|
+
# Declare every prefix that the content uses on a wrapper element, so the
|
|
256
|
+
# parser accepts the content, then let Nokogiri remove the namespaces from
|
|
257
|
+
# both the elements and the attributes. Return the content unchanged when
|
|
258
|
+
# it does not parse, which keeps the conservative behaviour of the
|
|
259
|
+
# relaton-bib sanitizer for text that only looks like markup.
|
|
260
|
+
#
|
|
261
|
+
# @param [String] cnt The markup.
|
|
262
|
+
#
|
|
263
|
+
# @return [String] The markup without namespace prefixes.
|
|
264
|
+
#
|
|
265
|
+
def drop_namespaces(cnt)
|
|
266
|
+
doc = parse_with_prefixes(cnt) or return cnt
|
|
267
|
+
|
|
268
|
+
doc.remove_namespaces!
|
|
269
|
+
doc.root.children.map do |c|
|
|
270
|
+
c.to_xml encoding: "UTF-8", save_with: SAVE_OPTS
|
|
271
|
+
end.join
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
#
|
|
275
|
+
# Parse markup that uses undeclared namespace prefixes.
|
|
276
|
+
#
|
|
277
|
+
# @param [String] cnt The markup.
|
|
278
|
+
#
|
|
279
|
+
# @return [Nokogiri::XML::Document, nil] The document, or nil when the
|
|
280
|
+
# content uses no prefix or does not parse.
|
|
281
|
+
#
|
|
282
|
+
def parse_with_prefixes(cnt)
|
|
283
|
+
prefixes = cnt.scan(NS_PREFIX_RE).flatten.uniq - ["xmlns"]
|
|
284
|
+
return if prefixes.empty?
|
|
285
|
+
|
|
286
|
+
decl = prefixes.map { |p| %(xmlns:#{p}="#{format NS_PLACEHOLDER, p}") }
|
|
287
|
+
doc = Nokogiri::XML "<r #{decl.join ' '}>#{cnt}</r>"
|
|
288
|
+
doc if doc.errors.empty?
|
|
289
|
+
end
|
|
290
|
+
|
|
216
291
|
#
|
|
217
292
|
# Parse a docidentifier from the source hash.
|
|
218
293
|
#
|
|
@@ -303,9 +378,10 @@ module Relaton
|
|
|
303
378
|
def parse_abstract
|
|
304
379
|
return [] unless @src["abstract"]
|
|
305
380
|
|
|
306
|
-
content = @src["abstract"]
|
|
307
381
|
abstract = Bib::Abstract.new(
|
|
308
|
-
content:
|
|
382
|
+
content: normalize_markup(@src["abstract"]),
|
|
383
|
+
language: "en",
|
|
384
|
+
script: "Latn",
|
|
309
385
|
)
|
|
310
386
|
[abstract]
|
|
311
387
|
end
|
|
@@ -448,8 +524,10 @@ module Relaton
|
|
|
448
524
|
# @return [Bib::Organization] The organization.
|
|
449
525
|
#
|
|
450
526
|
def create_org(name, abbreviation = nil)
|
|
451
|
-
n = [Bib::TypedLocalizedString.new(content: name)]
|
|
452
|
-
a =
|
|
527
|
+
n = [Bib::TypedLocalizedString.new(content: CGI.unescapeHTML(name))]
|
|
528
|
+
a = if abbreviation
|
|
529
|
+
Bib::LocalizedString.new(content: CGI.unescapeHTML(abbreviation))
|
|
530
|
+
end
|
|
453
531
|
Bib::Organization.new name: n, abbreviation: a
|
|
454
532
|
end
|
|
455
533
|
|
|
@@ -462,11 +540,7 @@ module Relaton
|
|
|
462
540
|
return [] unless @src["standards-body"]
|
|
463
541
|
|
|
464
542
|
name, acronym = @src["standards-body"].values_at("name", "acronym")
|
|
465
|
-
|
|
466
|
-
CGI.unescapeHTML(name),
|
|
467
|
-
acronym && CGI.unescapeHTML(acronym),
|
|
468
|
-
)
|
|
469
|
-
[contributor(org, "authorizer")]
|
|
543
|
+
[contributor(create_org(name, acronym), "authorizer")]
|
|
470
544
|
end
|
|
471
545
|
|
|
472
546
|
#
|
|
@@ -728,7 +802,8 @@ module Relaton
|
|
|
728
802
|
|
|
729
803
|
@src["container-title"].map do |ct|
|
|
730
804
|
contrib = create_authors_editors false, "editor"
|
|
731
|
-
|
|
805
|
+
title = Bib::Title.new content: normalize_markup(ct)
|
|
806
|
+
bib = Bib::ItemBase.new(title: [title], contributor: contrib)
|
|
732
807
|
Bib::Relation.new(type: "includedIn", bibitem: bib)
|
|
733
808
|
end
|
|
734
809
|
end
|
|
@@ -777,7 +852,7 @@ module Relaton
|
|
|
777
852
|
else []
|
|
778
853
|
end
|
|
779
854
|
con_ttl.map do |ct|
|
|
780
|
-
title = Bib::Title.new content: ct
|
|
855
|
+
title = Bib::Title.new content: normalize_markup(ct)
|
|
781
856
|
Bib::Series.new title: [title], abbreviation: abbrev
|
|
782
857
|
end
|
|
783
858
|
end
|
data/lib/relaton/doi/version.rb
CHANGED
data/relaton-doi.gemspec
CHANGED
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
# Uncomment to register a new dependency of your gem
|
|
35
35
|
# spec.add_dependency "example-gem", "~> 1.0"
|
|
36
36
|
|
|
37
|
+
spec.add_dependency "relaton-bib", "~> 2.1.8"
|
|
37
38
|
spec.add_dependency "relaton-bipm", "~> 2.1.0"
|
|
38
39
|
spec.add_dependency "relaton-ieee", "~> 2.1.0"
|
|
39
40
|
spec.add_dependency "relaton-ietf", "~> 2.1.0"
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: relaton-doi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: relaton-bib
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 2.1.8
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 2.1.8
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: relaton-bipm
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|