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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45cbebdc6eb8297d87639980cfe4b04bd4664605dd72a31c5f16684abe3d6aeb
4
- data.tar.gz: 7a91de197590962abd143f6a21dc9272b9fa7e7739c025f016856332bdec045e
3
+ metadata.gz: 953aa1acd54cd1a21fb180ba132e24da1a1f9bb310496bfc4bc454e6d8d73621
4
+ data.tar.gz: 3a60c3a21e9eb9b1b32bc62a57bdea14f8efbad2beddf490648dbc073c63cf7d
5
5
  SHA512:
6
- metadata.gz: b11441ab2a84b958dbf57b76bc4dbf9bf729ad8d45e40edd3b7fa611904ee1438e1a7457c528ef5fb9b337148a145c5a69624f1f7441274fbdbfab00c7892990
7
- data.tar.gz: 4abce09819c596c70435751f2d0b66eb07b23b6a00910ff8b18bcf6fb91aac6e51b7453c69f589f2bfe6e214f1c61af3e3b84a0d8e1a1dbc8d7ea7f28c938d19
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.
@@ -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: content, language: "en", script: "Latn",
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 = abbreviation ? Bib::LocalizedString.new(content: abbreviation) : nil
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
- org = create_org(
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
- bib = Bib::ItemBase.new(title: [Bib::Title.new(content: ct)], contributor: contrib)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Relaton
4
4
  module Doi
5
- VERSION = "2.1.1"
5
+ VERSION = "2.1.3"
6
6
  end
7
7
  end
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.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-05-05 00:00:00.000000000 Z
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