relaton 3.0.0.pre.alpha.4 → 3.0.0.pre.alpha.5

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/adobe/processor.rb +9 -0
  3. data/lib/relaton/bib/converter/csl.rb +110 -0
  4. data/lib/relaton/bib/converter/ris.rb +104 -0
  5. data/lib/relaton/bib/converter/titles.rb +24 -0
  6. data/lib/relaton/bib/item_data.rb +9 -0
  7. data/lib/relaton/bib/model/item.rb +6 -6
  8. data/lib/relaton/bib/sanitizer.rb +71 -30
  9. data/lib/relaton/bib.rb +10 -0
  10. data/lib/relaton/bipm/processor.rb +1 -0
  11. data/lib/relaton/bipm/rawdata_bipm_metrologia/affiliations.rb +6 -6
  12. data/lib/relaton/bipm/si_brochure_parser.rb +2 -2
  13. data/lib/relaton/bsi/processor.rb +5 -0
  14. data/lib/relaton/ccsds/bibliography.rb +1 -0
  15. data/lib/relaton/ccsds/processor.rb +12 -0
  16. data/lib/relaton/cen/hit_collection.rb +1 -1
  17. data/lib/relaton/cen/processor.rb +5 -0
  18. data/lib/relaton/cen/scraper.rb +9 -9
  19. data/lib/relaton/cie/data_fetcher.rb +18 -18
  20. data/lib/relaton/cie/processor.rb +1 -0
  21. data/lib/relaton/cie.rb +1 -1
  22. data/lib/relaton/cloud.rb +127 -0
  23. data/lib/relaton/core/hit_collection.rb +6 -10
  24. data/lib/relaton/core/processor.rb +68 -0
  25. data/lib/relaton/db/cache.rb +444 -148
  26. data/lib/relaton/db/cache_entry.rb +33 -0
  27. data/lib/relaton/db/registry.rb +52 -0
  28. data/lib/relaton/db.rb +131 -72
  29. data/lib/relaton/doi/crossref.rb +23 -6
  30. data/lib/relaton/doi/processor.rb +1 -0
  31. data/lib/relaton/easc/processor.rb +1 -0
  32. data/lib/relaton/ecma/data_fetcher.rb +1 -1
  33. data/lib/relaton/ecma/data_parser.rb +1 -1
  34. data/lib/relaton/ecma/edition_parser.rb +2 -2
  35. data/lib/relaton/ecma/memento_parser.rb +4 -4
  36. data/lib/relaton/ecma/standard_parser.rb +6 -6
  37. data/lib/relaton/etsi/processor.rb +1 -0
  38. data/lib/relaton/gb/gb_scraper.rb +5 -5
  39. data/lib/relaton/gb/scraper.rb +16 -16
  40. data/lib/relaton/gb/sec_scraper.rb +8 -8
  41. data/lib/relaton/gb/t_scraper.rb +5 -5
  42. data/lib/relaton/gost/processor.rb +1 -0
  43. data/lib/relaton/iala/processor.rb +1 -0
  44. data/lib/relaton/iana/data_fetcher.rb +3 -3
  45. data/lib/relaton/iana/parser.rb +8 -4
  46. data/lib/relaton/iana/processor.rb +9 -0
  47. data/lib/relaton/iec/data_parser.rb +24 -9
  48. data/lib/relaton/iec/processor.rb +6 -0
  49. data/lib/relaton/iec.rb +1 -1
  50. data/lib/relaton/ieee/data_fetcher.rb +1 -1
  51. data/lib/relaton/ieee/processor.rb +8 -0
  52. data/lib/relaton/ietf/data_fetcher.rb +1 -1
  53. data/lib/relaton/ietf/processor.rb +1 -0
  54. data/lib/relaton/ietf/rfc/entry.rb +15 -19
  55. data/lib/relaton/iho/processor.rb +1 -0
  56. data/lib/relaton/index/file_io.rb +2 -2
  57. data/lib/relaton/index/pool.rb +4 -3
  58. data/lib/relaton/index/shard_source.rb +1 -1
  59. data/lib/relaton/index/type.rb +2 -5
  60. data/lib/relaton/isbn/open_library.rb +11 -7
  61. data/lib/relaton/isbn/processor.rb +10 -0
  62. data/lib/relaton/iso/data_parser.rb +2 -2
  63. data/lib/relaton/iso/processor.rb +5 -0
  64. data/lib/relaton/iso/scraper.rb +20 -20
  65. data/lib/relaton/itu/bibliography.rb +99 -13
  66. data/lib/relaton/itu/data_crawler_r.rb +2 -2
  67. data/lib/relaton/itu/hit_collection.rb +64 -52
  68. data/lib/relaton/itu/processor.rb +1 -0
  69. data/lib/relaton/itu/scraper.rb +13 -3
  70. data/lib/relaton/itu.rb +0 -2
  71. data/lib/relaton/jis/data_fetcher.rb +4 -4
  72. data/lib/relaton/jis/processor.rb +1 -0
  73. data/lib/relaton/jis/scraper.rb +8 -8
  74. data/lib/relaton/oasis/browser_agent.rb +2 -2
  75. data/lib/relaton/oasis/data_parser.rb +5 -5
  76. data/lib/relaton/oasis/data_parser_utils.rb +2 -2
  77. data/lib/relaton/oasis/data_part_parser.rb +7 -7
  78. data/lib/relaton/ogc/processor.rb +7 -0
  79. data/lib/relaton/oiml/processor.rb +1 -0
  80. data/lib/relaton/omg/scraper.rb +11 -11
  81. data/lib/relaton/omg.rb +1 -1
  82. data/lib/relaton/plateau/processor.rb +1 -0
  83. data/lib/relaton/un/bibliography.rb +21 -10
  84. data/lib/relaton/un/processor.rb +1 -0
  85. data/lib/relaton/version.rb +1 -1
  86. data/lib/relaton/w3c/processor.rb +1 -0
  87. data/lib/relaton.rb +13 -0
  88. metadata +48 -16
  89. data/lib/relaton/itu/pubid.rb +0 -199
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
3
+ require "moxml"
4
4
  require_relative "../iso"
5
5
  require_relative "scraper"
6
6
 
@@ -331,7 +331,7 @@ module Relaton
331
331
  end
332
332
 
333
333
  def strip_html(html)
334
- Nokogiri::HTML.fragment(html).text.strip.gsub(/\s+/, " ")
334
+ Moxml.new.parse_fragment(html).map(&:text).join.strip.gsub(/\s+/, " ")
335
335
  end
336
336
 
337
337
  # ---- copyright -------------------------------------------------------
@@ -39,6 +39,11 @@ module Relaton
39
39
  DataFetcher.fetch(source, **opts)
40
40
  end
41
41
 
42
+ # `Bibliography.get` applies a year to the document the reference names.
43
+ def fold_year(pubid, year)
44
+ fold_year_on_root pubid, year
45
+ end
46
+
42
47
  # @param xml [String]
43
48
  # @return [RelatonIsoBib::IsoBibliographicItem]
44
49
  def from_xml(xml)
@@ -95,7 +95,7 @@ module Relaton
95
95
  def id
96
96
  return @id if defined?(@id)
97
97
 
98
- did = @doc.at("//h1/span[1]")
98
+ did = @doc.at_xpath("//h1/span[1]")
99
99
  @errors[:id] &&= did.nil?
100
100
  @id = did && did.text.split(" | ").first.strip
101
101
  end
@@ -117,7 +117,7 @@ module Relaton
117
117
  def edition
118
118
  return @edition if defined?(@edition)
119
119
 
120
- ed = @doc.at("//div[div[.='Edition']]/text()[last()]")
120
+ ed = @doc.at_xpath("//div[div[.='Edition']]/text()[last()]")
121
121
  @errors[:edition] &&= ed.nil?
122
122
  @edition = ed && Bib::Edition.new(content: ed.text.match(/\d+$/).to_s)
123
123
  end
@@ -172,7 +172,7 @@ module Relaton
172
172
  langs = languages.each_with_object([]) do |l, s|
173
173
  # Don't need to get page for en. We already have it.
174
174
  d = l[:path] ? get_page(l[:path])[0] : @doc
175
- unless d.at("//h5[@class='help-block'][.='недоступно на русском языке']")
175
+ unless d.at_xpath("//h5[@class='help-block'][.='недоступно на русском языке']")
176
176
  s << l
177
177
  titles += fetch_title(d, l[:lang])
178
178
 
@@ -208,7 +208,7 @@ module Relaton
208
208
 
209
209
  # Get page.
210
210
  # @param path [String] page's path
211
- # @return [Array<Nokogiri::HTML::Document, String>]
211
+ # @return [Array<Moxml::Document, String>]
212
212
  def get_page(path) # rubocop:disable Metrics/MethodLength
213
213
  try = 0
214
214
  begin
@@ -276,12 +276,12 @@ module Relaton
276
276
  # @param [Net::HTTPOK] resp HTTP response
277
277
  # @param [URI::HTTPS] uri URI of the page
278
278
  #
279
- # @return [Nokogiri::HTML4::Document] document
279
+ # @return [Moxml::Document] document
280
280
  # @raise [Relaton::RequestError] if the page could not be parsed
281
281
  #
282
282
  def try_if_fail(resp, uri)
283
283
  10.times do
284
- doc = Nokogiri::HTML(resp.body)
284
+ doc = Moxml.new.parse_html(resp.body)
285
285
  # stop trying if page has a document id
286
286
  return doc if item_ref(doc)
287
287
 
@@ -312,12 +312,12 @@ module Relaton
312
312
  #
313
313
  # Parse ID from the document.
314
314
  #
315
- # @param [Nokogiri::HTML::Document] doc document to parse
315
+ # @param [Moxml::Document] doc document to parse
316
316
  #
317
317
  # @return [String, nil] ID
318
318
  #
319
319
  def item_ref(doc)
320
- ref = doc.at("//main//section/div/div/div//h1/span[1]")
320
+ ref = doc.at_xpath("//main//section/div/div/div//h1/span[1]")
321
321
  @errors[:reference] &&= ref.nil?
322
322
  ref&.text&.strip
323
323
  end
@@ -334,7 +334,7 @@ module Relaton
334
334
  def stage_code
335
335
  return @stage_code if defined?(@stage_code)
336
336
 
337
- stc = @doc.at("//ul[@class='dropdown-menu']/li[@class='active']/a/span[@class='stage-code']")
337
+ stc = @doc.at_xpath("//ul[@class='dropdown-menu']/li[@class='active']/a/span[@class='stage-code']")
338
338
  @errors[:stage] &&= stc.nil?
339
339
  @stage_code = stc&.text
340
340
  end
@@ -351,7 +351,7 @@ module Relaton
351
351
  rels = @doc.xpath(
352
352
  "//ul[@class='steps']/li", "//div[contains(@class, 'sub-step')]"
353
353
  ).reduce([]) do |a, r|
354
- type, date = relation_type(r.at("h4", "h5").text.strip)
354
+ type, date = relation_type(r.at_xpath("h4|h5").text.strip)
355
355
  next a if types.include?(type)
356
356
 
357
357
  a + create_relations(r, type, date)
@@ -383,7 +383,7 @@ module Relaton
383
383
  #
384
384
  # Create relations.
385
385
  #
386
- # @param [Nokogiri::HTML::Element] rel relation element
386
+ # @param [Moxml::Element] rel relation element
387
387
  # @param [String] type relation type
388
388
  # @param [Hash{Symbol=>String}] date relation document date
389
389
  # @option date [String] :type date type
@@ -400,7 +400,7 @@ module Relaton
400
400
  end
401
401
 
402
402
  # Fetch titles.
403
- # @param doc [Nokogiri::HTML::Document]
403
+ # @param doc [Moxml::Document]
404
404
  # @param lang [String]
405
405
  # @return [Array<RelatonBib::TypedTitleString>]
406
406
  def fetch_title(doc, lang) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
@@ -417,7 +417,7 @@ module Relaton
417
417
  end
418
418
 
419
419
  def parse_titles(doc)
420
- # head = doc.at "//nav[contains(@class,'heading-condensed')]"
420
+ # head = doc.at_xpath "//nav[contains(@class,'heading-condensed')]"
421
421
  ttls = doc.xpath("//h1[@class='stdTitle']/span[position()>1]").map(&:text)
422
422
  return ttls if @errors[:title] &&= ttls.empty?
423
423
 
@@ -455,14 +455,14 @@ module Relaton
455
455
  def fetch_dates # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
456
456
  dates = []
457
457
  %r{^[^\s]+\s[\d-]+:(?<ref_date_str>\d{4})} =~ id
458
- pub_date_str = @doc.at("//span[@itemprop='releaseDate']")
458
+ pub_date_str = @doc.at_xpath("//span[@itemprop='releaseDate']")
459
459
  @errors[:date_pub] &&= pub_date_str.nil?
460
460
  if ref_date_str
461
461
  dates += parse_date_from_id ref_date_str, pub_date_str
462
462
  elsif pub_date_str
463
463
  dates << Bib::Date.new(type: "published", at: pub_date_str.text)
464
464
  end
465
- corr_data = @doc.at "//span[@itemprop='dateModified']"
465
+ corr_data = @doc.at_xpath "//span[@itemprop='dateModified']"
466
466
  @errors[:date_corr] &&= corr_data.nil?
467
467
  dates << Bib::Date.new(type: "corrected", at: corr_data.text) if corr_data
468
468
  dates
@@ -522,13 +522,13 @@ module Relaton
522
522
  #
523
523
  def fetch_source(url) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
524
524
  source = [Bib::Uri.new(type: "src", content: url)]
525
- obp = @doc.at("//a[.='Read sample']")
525
+ obp = @doc.at_xpath("//a[.='Read sample']")
526
526
  @errors[:link_obp] &&= obp.nil?
527
527
  source << Bib::Uri.new(type: "obp", content: obp[:href]) if obp
528
- rss = @doc.at("//a[contains(@href, 'rss')]")
528
+ rss = @doc.at_xpath("//a[contains(@href, 'rss')]")
529
529
  @errors[:link_rss] &&= rss.nil?
530
530
  source << Bib::Uri.new(type: "rss", content: DOMAIN + rss[:href]) if rss
531
- pub = @doc.at "//p[contains(., 'publicly available')]/a",
531
+ pub = @doc.at_xpath "//p[contains(., 'publicly available')]/a",
532
532
  "//p[contains(., 'can be downloaded from the')]/a"
533
533
  @errors[:link_pub] &&= pub.nil?
534
534
  source << Bib::Uri.new(type: "pub", content: pub[:href]) if pub
@@ -542,7 +542,7 @@ module Relaton
542
542
  owner_name = ref.match(/.*?(?=\s)/).to_s
543
543
  from = ref.match(/(?<=:)\d{4}/).to_s
544
544
  if from.empty?
545
- date = @doc.at(
545
+ date = @doc.at_xpath(
546
546
  "//span[@itemprop='releaseDate']",
547
547
  "//ul[@id='stages']/li[contains(@class,'active')]/ul/li[@class='active']/a/span[@class='stage-date']",
548
548
  )
@@ -585,7 +585,7 @@ module Relaton
585
585
  # @return [Relaton::Bib::Contributor, nil]
586
586
  #
587
587
  def fetch_editorialgroup_contributor # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
588
- wg = @doc.at(
588
+ wg = @doc.at_xpath(
589
589
  "//div[contains(., 'Technical Committe')]" \
590
590
  "/following-sibling::span/a",
591
591
  )
@@ -5,12 +5,28 @@ module Relaton
5
5
  module Bibliography
6
6
  extend self
7
7
 
8
- # @param refid [Relaton::Itu::Pubid, String] a document reference
8
+ # Supplements that print the date of the recommendation they amend
9
+ # first, "ITU-T G.989.2 (2014) Amd. 1 (04/2016)": their edition is the
10
+ # base's.
11
+ AMENDING = [
12
+ ::Pubid::Itu::Identifiers::Amendment, ::Pubid::Itu::Identifiers::Corrigendum,
13
+ ::Pubid::Itu::Identifiers::Errata, ::Pubid::Itu::Identifiers::Addendum,
14
+ ].freeze
15
+
16
+ # Parts of a recommendation that print its date first when it has one,
17
+ # "ITU-T Z.100 (06/2021) Annex F1", and their own date otherwise.
18
+ ANNEXED = [
19
+ ::Pubid::Itu::Identifiers::AnnexOfRecommendation,
20
+ ::Pubid::Itu::Identifiers::AppendixOfRecommendation,
21
+ ].freeze
22
+
23
+ # @param refid [Pubid::Itu::Identifier, String] a document reference; a
24
+ # String that Pubid::Itu cannot parse raises Pubid::Errors::ParseError
9
25
  # @return [Relaton::Itu::HitCollection]
10
26
  def search(refid)
11
27
  if refid.is_a? String
12
28
  warn_incorrect_ref(refid)
13
- refid = Pubid.parse refid
29
+ refid = ::Pubid::Itu.parse refid
14
30
  end
15
31
  HitCollection.new(refid).tap(&:search)
16
32
  end
@@ -21,19 +37,60 @@ module Relaton
21
37
  # @return [Relaton::Bib::ItemData, nil]
22
38
  def get(code, year = nil, opts = {})
23
39
  warn_incorrect_ref(code)
24
- refid = Pubid.parse code
25
- refid.year ||= year
40
+ refid = with_year ::Pubid::Itu.parse(code), year
26
41
 
27
42
  ret = itubib_get1(refid)
28
43
  return nil if ret.nil?
29
44
 
30
- ret = ret.to_most_recent_reference unless refid.year || opts[:keep_year]
45
+ unless edition_year(refid) || opts[:keep_year]
46
+ ret = ret.to_most_recent_reference
47
+ end
31
48
  ret = ret.to_all_parts if opts[:all_parts]
32
49
  ret
33
50
  end
34
51
 
52
+ # The year a reference selects its hits by. It is read the way a hit
53
+ # code's first date is: for an amending supplement, it is the year of
54
+ # the recommendation it amends. HitCollection uses it too.
55
+ #
56
+ # @param refid [Pubid::Itu::Identifier]
57
+ # @return [String, nil]
58
+ def edition_year(refid)
59
+ return refid.base&.year if amending?(refid)
60
+ return refid.base&.year || refid.year if annexed?(refid)
61
+
62
+ refid.year
63
+ end
64
+
65
+ # The identifier a reference is a part of: the recommendation an
66
+ # amending supplement, an annex or an appendix belongs to; else itself.
67
+ # A series or recommendation supplement is a document of its own.
68
+ #
69
+ # @param refid [Pubid::Itu::Identifier]
70
+ # @return [Pubid::Itu::Identifier]
71
+ def document(refid)
72
+ amending?(refid) || annexed?(refid) ? refid.base : refid
73
+ end
74
+
35
75
  private
36
76
 
77
+ # @param refid [Pubid::Itu::Identifier]
78
+ # @param year [String, nil] the year passed beside the reference
79
+ # @return [Pubid::Itu::Identifier] refid, dated with year when it has
80
+ # no year of its own
81
+ def with_year(refid, year)
82
+ return refid if year.nil? || edition_year(refid)
83
+
84
+ hash = refid.to_hash
85
+ dated = amending?(refid) ? hash["base"] : hash
86
+ dated["year"] = year.to_s
87
+ ::Pubid::Itu::Identifier.from_hash hash
88
+ end
89
+
90
+ def amending?(refid) = AMENDING.any? { |klass| refid.is_a? klass }
91
+
92
+ def annexed?(refid) = ANNEXED.any? { |klass| refid.is_a? klass }
93
+
37
94
  def warn_incorrect_ref(ref)
38
95
  if ref =~ /(ITU[\s-]T\s\w)\.(Suppl\.|Annex)\s?(\w?\d+)/
39
96
  correct_ref = "#{$~[1]} #{$~[2]} #{$~[3]}"
@@ -45,30 +102,59 @@ module Relaton
45
102
  Util.info "Not found.", key: refid.to_s
46
103
  # one hit per edition means a year can repeat (a recommendation and its
47
104
  # amendment); the requested year itself is not a "different year" hint
48
- years = missed_years.uniq.reject { |y| y == refid.year }
105
+ year = edition_year(refid)
106
+ years = missed_years.uniq.reject { |y| y == year }
49
107
  if years.any?
50
108
  plural = years.size > 1 ? "s" : ""
51
- Util.info "There was no match for `#{refid.year}` year, though there were matches " \
52
- "found for `#{years.join('`, `')}` year#{plural}.", key: refid.to_s
109
+ Util.info "There was no match for `#{year}` year, though there " \
110
+ "were matches found for `#{years.join('`, `')}` " \
111
+ "year#{plural}.", key: refid.to_s
53
112
  end
54
113
  nil
55
114
  end
56
115
 
116
+ # Keep the hits that are the referenced document, in any edition:
117
+ # #isobib_results_filter picks the year afterwards. The date is ignored,
118
+ # and the version only when the reference names none. Everything else
119
+ # must be equal, so a supplement, or a Report for a Recommendation, never
120
+ # matches.
121
+ #
122
+ # Not `===`: it reads a missing `subseries` as "any value", so
123
+ # `ITU-T H.264` would keep `ITU-T H.264.1`.
57
124
  def search_filter(refid)
58
- result = search(refid)
59
- result.select do |i|
125
+ ignore = %i[year month day]
126
+ ignore << :version unless stated_version?(refid)
127
+ search(refid).select do |i|
60
128
  next true unless i.hit[:code]
61
129
 
62
- pubid = Pubid.parse i.hit[:code]
63
- refid.===(pubid, [:year])
130
+ hit = ::Pubid::Itu.parse(i.hit[:code])
131
+ refid.matches?(hit, ignore: ignore) && same_own_date?(refid, hit)
132
+ rescue ::Pubid::Errors::ParseError
133
+ false # a live hit ITU spells in a form nothing can identify
64
134
  end
65
135
  end
66
136
 
137
+ # A version on the reference, or on the recommendation it amends.
138
+ def stated_version?(refid)
139
+ id = document(refid)
140
+ id.respond_to?(:version) && !id.version.nil?
141
+ end
142
+
143
+ # `ignore:` drops the date at every level, but an amending supplement's
144
+ # own date tells two "Amd. 1" to different editions apart.
145
+ def same_own_date?(refid, hit)
146
+ return true unless amending?(refid)
147
+
148
+ own = refid.to_hash.slice("year", "month", "day")
149
+ own.all? { |key, value| hit.to_hash[key] == value }
150
+ end
151
+
67
152
  def isobib_results_filter(result, refid)
68
153
  missed_years = []
154
+ year = edition_year(refid)
69
155
  result.each do |r|
70
156
  /\((?:\d{2}\/)?(?<pyear>\d{4})\)/ =~ r.hit[:code]
71
- if !refid.year || refid.year == pyear
157
+ if !year || year == pyear
72
158
  ret = r.item
73
159
  return { ret: ret } if ret
74
160
  end
@@ -564,7 +564,7 @@ module Relaton
564
564
  # the first cell, the title in the second.
565
565
  #
566
566
  # @param url [String]
567
- # @return [Array<Array(String, Nokogiri::XML::Element, Nokogiri::XML::NodeSet)>]
567
+ # @return [Array<Array(String, Moxml::Element, Moxml::NodeSet)>]
568
568
  def rows(url)
569
569
  get(url).search("//a[contains(@href,'parent=R-')]").filter_map do |a|
570
570
  id = a[:href].to_s[PARENT_RE, :id]
@@ -576,7 +576,7 @@ module Relaton
576
576
  # The title cell carries an optional red annotation ("Note - Suppressed on
577
577
  # …"), which is not part of the title.
578
578
  #
579
- # @param cell [Nokogiri::XML::Element, nil]
579
+ # @param cell [Moxml::Element, nil]
580
580
  # @return [String]
581
581
  def title_text(cell)
582
582
  return "" unless cell
@@ -24,21 +24,21 @@ module Relaton
24
24
  RECEDITIONS_URL = "#{DOMAIN}/mws/api/recommendations/getRecEditions?idrec=%<idrec>s&lang=en".freeze
25
25
  HANDLE_URL = "http://handle.itu.int/11.1002/1000/%<idrec>s-en".freeze
26
26
 
27
+ # `ref` is a Pubid::Itu identifier; its class and sector pick the route.
27
28
  def search
28
- case ref.to_ref
29
- # `ref` is a parsed Pubid, so only the abbreviated `OB.N` form survives
30
- # here — the local grammar reduces "Operational Bulletin 1096" to the
31
- # code "Operational" and drops the year, which no route can resolve.
32
- when %r{^ITU-R\sRR}, %r{\bOB\.} then request_publication
33
- when /^ITU-T/ then request_recommendation
34
- # A Report reference renders "Report ITU-R …" (the discriminator leads,
35
- # as ITU cites it), so it no longer starts with the sector — but it is
36
- # still served from the index like any other ITU-R document.
37
- when /^ITU-R\s/, /^Report\sITU-R\s/ then request_document
29
+ if publication?(ref) then request_publication
30
+ # an annex to a bulletin has no landing page, and it is not a
31
+ # recommendation either
32
+ elsif publication?(ref.root) then @array = []
33
+ else
34
+ case sector
35
+ when "T" then request_recommendation
36
+ when "R" then request_document
37
+ end
38
38
  end
39
39
  rescue Mechanize::ResponseCodeError, SocketError, Timeout::Error, Errno::ECONNRESET,
40
40
  EOFError, Net::ProtocolError, OpenSSL::SSL::SSLError => e
41
- raise Relaton::RequestError, "Could not access #{ref.to_ref}: #{e.message}"
41
+ raise Relaton::RequestError, "Could not access #{ref}: #{e.message}"
42
42
  end
43
43
 
44
44
  def agent
@@ -70,6 +70,21 @@ module Relaton
70
70
 
71
71
  private
72
72
 
73
+ # Radio Regulations and Operational Bulletins live on www.itu.int/pub,
74
+ # not in the dataset.
75
+ def publication?(id)
76
+ id.is_a?(::Pubid::Itu::Identifiers::RadioRegulations) ||
77
+ id.is_a?(::Pubid::Itu::Identifiers::SpecialPublication)
78
+ end
79
+
80
+ # An annex or an appendix carries its sector on the recommendation it
81
+ # belongs to.
82
+ #
83
+ # @return [String, nil] "T" or "R"
84
+ def sector
85
+ (ref.sector || ref.root.sector)&.to_s
86
+ end
87
+
73
88
  # ITU-T Recommendations and supplements. The index is preferred (offline,
74
89
  # every edition, no scraping); the live path covers what the dataset does
75
90
  # not have — Implementers' Guides, the `(V##)`/`Annex` forms the data
@@ -86,9 +101,8 @@ module Relaton
86
101
 
87
102
  # @return [Array<Relaton::Itu::Hit>] newest edition first
88
103
  def index_hits
89
- pubid = index_pubid_ref
90
- return [] unless pubid
91
-
104
+ # Every edition is a candidate; Bibliography selects the year.
105
+ pubid = ref.exclude(:year, :month, :day)
92
106
  Util.info "Fetching from Relaton repository ...", key: ref.to_s
93
107
  index.search(pubid) { |i| index_match?(pubid, i[:id]) }
94
108
  .map { |r| index_hit r }.sort_by { |h| edition_key h.hit[:code] }.reverse
@@ -112,7 +126,10 @@ module Relaton
112
126
  def year_satisfied?(hits)
113
127
  return false if hits.empty?
114
128
 
115
- ref.year.nil? || hits.any? { |h| edition_key(h.hit[:code]).first == ref.year.to_i }
129
+ year = Bibliography.edition_year(ref)
130
+ return true if year.nil?
131
+
132
+ hits.any? { |h| edition_key(h.hit[:code]).first == year.to_i }
116
133
  end
117
134
 
118
135
  # The edition date of a rendered docidentifier, as `[year, month]`. Uses
@@ -135,14 +152,13 @@ module Relaton
135
152
 
136
153
  def request_document # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
137
154
  Util.info "Fetching from Relaton repository ...", key: ref.to_s
138
- # Pass the reference's parsed pubid (not a String) so Relaton::Index can
155
+ # Pass the reference as a pubid (not a String) so Relaton::Index can
139
156
  # narrow candidates by document number before the block; `index_match?`
140
157
  # then matches every edition when the reference omits the part and the
141
158
  # exact edition when it names one. Rows are Pubid::Itu objects (not
142
159
  # Comparable), so rank by the numeric edition in `code.parts` (`["3"]` → 3)
143
160
  # to return the latest — index order isn't by edition.
144
- pubid = pubid_ref
145
- row = index.search(pubid) { |i| index_match?(pubid, i[:id]) }
161
+ row = index.search(ref) { |i| index_match?(ref, i[:id]) }
146
162
  .max_by { |i| i[:id].code&.parts&.last.to_i }
147
163
  return unless row
148
164
 
@@ -155,30 +171,6 @@ module Relaton
155
171
  @array = [hit]
156
172
  end
157
173
 
158
- # Parse the reference into a Pubid::Itu identifier for the index lookup. A
159
- # ref pubid can't parse raises (a `Pubid`/`Parslet` error) and propagates to
160
- # the caller (relaton-cli / API callers rescue it), mirroring the ETSI
161
- # flavor — the consumer no longer degrades to a raw-string substring search.
162
- #
163
- # @return [::Pubid::Itu::Identifier]
164
- def pubid_ref
165
- ::Pubid::Itu.parse ref.to_ref
166
- end
167
-
168
- # The reference as a pubid for an ITU-T index lookup, stripped of its
169
- # edition date (every edition is a candidate; Bibliography selects the
170
- # year). Unlike `#pubid_ref` this **rescues** an unparseable reference: the
171
- # ITU-T path has a live fallback to degrade to, and a malformed reference
172
- # (e.g. `ITU-T G.Suppl.47`) should warn and report "Not found", not raise.
173
- #
174
- # @return [::Pubid::Itu::Identifier, nil]
175
- def index_pubid_ref
176
- undated = ref.dup.tap { |r| r.year = r.month = r.day = nil }
177
- ::Pubid::Itu.parse undated.to_ref
178
- rescue StandardError
179
- nil
180
- end
181
-
182
174
  # Does an index row's id match the reference? When the reference omits the
183
175
  # part/edition (a bare `ITU-R P.838`), every edition of that exact document
184
176
  # matches — "search all parts"; when it names a part (`ITU-R P.838-2`), only
@@ -227,9 +219,15 @@ module Relaton
227
219
  nil
228
220
  end
229
221
 
230
- # @return [String] the `rec=` value for the rec.aspx lookup
222
+ # The `rec=` value for the rec.aspx lookup: the document without its
223
+ # sector, edition date or version — "H.264", "A Suppl. 2",
224
+ # "G.989 Suppl. 1". An amendment or an annex is looked up by the
225
+ # recommendation it belongs to.
226
+ #
227
+ # @return [String]
231
228
  def rec_query
232
- ref.suppl ? "#{ref.code} Suppl. #{ref.suppl}" : ref.code
229
+ Bibliography.document(ref).exclude(:year, :month, :day, :version)
230
+ .to_s.delete_prefix("ITU-T ")
233
231
  end
234
232
 
235
233
  # @param idrec [String]
@@ -244,10 +242,14 @@ module Relaton
244
242
  # @param edition [Hash] a getRecEditions entry
245
243
  # @return [Relaton::Itu::Hit]
246
244
  def recommendation_hit(edition)
245
+ # The crawler's rec_name canonicalisation ("G 231" -> "G.231"): the
246
+ # code is parsed with Pubid::Itu, which rejects the space spelling.
247
+ require_relative "data_parser_t"
248
+ name = DataParserT.normalize_rec_name edition["rec_name"]
247
249
  # No `:ref` key: it drives Hit#gi_imp, which would route an Implementers'
248
250
  # Guide reference onto the getImplGuides endpoints.
249
251
  Hit.new({
250
- code: "ITU-T #{edition['rec_name']}",
252
+ code: "ITU-T #{name}",
251
253
  title: edition["title"],
252
254
  url: format(HANDLE_URL, idrec: edition["idrec"]),
253
255
  type: "recommendation",
@@ -279,21 +281,31 @@ module Relaton
279
281
  end
280
282
 
281
283
  # The /pub identifier for a Radio Regulation or an Operational Bulletin
282
- # (`OB.1096` → `T-SP-OB.1096-2016`). nil — no hit — rather than a bogus URL
283
- # when the bulletin reference carries no number.
284
+ # (`OB.1096` → `T-SP-OB.1096-2016`). nil — no hit — for any other special
285
+ # publication, which has no derivable landing page.
284
286
  #
285
287
  # @return [String, nil]
286
288
  def publication_id
287
- return "R-REG-RR-#{ref.year}" if ref.code == "RR"
289
+ return "R-REG-RR-#{ref.year}" if radio_regulations?
288
290
 
289
- num = ref.code[/\d+/]
290
- "T-SP-OB.#{num}-#{ref.year}" if num
291
+ "T-SP-OB.#{ref.number}-#{ref.year}" if bulletin?
291
292
  end
292
293
 
293
- # @return [String] the docidentifier-friendly code (year only, no month)
294
+ # The docidentifier (year only, no month or day). A bulletin keeps its
295
+ # sector even when the reference is the sector-less `ITU OB No. 1096`.
296
+ #
297
+ # @return [String]
294
298
  def publication_code
295
- "#{ref.prefix}-#{ref.sector} #{ref.code} (#{ref.year})"
299
+ return "ITU-R RR (#{ref.year})" if radio_regulations?
300
+
301
+ "ITU-T OB.#{ref.number} (#{ref.year})"
296
302
  end
303
+
304
+ def radio_regulations?
305
+ ref.is_a? ::Pubid::Itu::Identifiers::RadioRegulations
306
+ end
307
+
308
+ def bulletin? = ref.series.to_s == "OB"
297
309
  end
298
310
  end
299
311
  end
@@ -6,6 +6,7 @@ module Relaton
6
6
  def initialize
7
7
  @short = :relaton_itu
8
8
  @prefix = "ITU"
9
+ @pubid_identifier = :Itu # Db cache key
9
10
  @defaultprefix = %r{^ITU\s}
10
11
  @idtype = "ITU"
11
12
  @datasets = %w[itu-r itu-t]
@@ -112,13 +112,23 @@ module Relaton
112
112
  type = "ISO"
113
113
  text.match(/[^(]+/).to_s.strip.squeeze(" ")
114
114
  else
115
- pubid = Pubid.parse(text)
116
- type = pubid.prefix
117
- pubid.to_s
115
+ type = "ITU"
116
+ canonical_id text
118
117
  end => id
119
118
  Docidentifier.new(type: type, content: id, primary: true)
120
119
  end
121
120
 
121
+ # The pubid spelling of an ITU code. The code is data from ITU, so a
122
+ # form pubid cannot identify is kept as ITU wrote it.
123
+ #
124
+ # @param text [String]
125
+ # @return [String]
126
+ def canonical_id(text)
127
+ ::Pubid::Itu.parse(text).to_s
128
+ rescue ::Pubid::Errors::ParseError
129
+ text
130
+ end
131
+
122
132
  # @return [Array<Relaton::Bib::Contributor>]
123
133
  def fetch_contributors
124
134
  contribs = fetch_publisher_contributors
data/lib/relaton/itu.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "mechanize"
2
- require "parslet"
3
2
  require "pubid"
4
3
  require "digest/md5"
5
4
  require "relaton/index"
@@ -11,7 +10,6 @@ require_relative "itu/item_data"
11
10
  require_relative "itu/model/item"
12
11
  require_relative "itu/model/bibitem"
13
12
  require_relative "itu/model/bibdata"
14
- require_relative "itu/pubid"
15
13
  require_relative "itu/scraper"
16
14
  require_relative "itu/hit_collection"
17
15
  require_relative "itu/bibliography"
@@ -114,12 +114,12 @@ module Relaton
114
114
  end
115
115
 
116
116
  def parse_offset(resp) # rubocop:disable Metrics/AbcSize
117
- if resp.at('//*[@id="btnPaging"]') # first page
117
+ if resp.at_xpath('//*[@id="btnPaging"]') # first page
118
118
  xpath = '//script[contains(.,"var count =")]'
119
- @count = resp.at(xpath).text.match(/var count = (\d+);/)[1]
120
- resp.at("//*[@id='offset']")[:value].to_i
119
+ @count = resp.at_xpath(xpath).text.match(/var count = (\d+);/)[1]
120
+ resp.at_xpath("//*[@id='offset']")[:value].to_i
121
121
  else
122
- script = resp.at("//script").text
122
+ script = resp.at_xpath("//script").text
123
123
  script.match(/\("offset"\)\.value = '(\d+)'/)[1].to_i
124
124
  end
125
125
  end
@@ -10,6 +10,7 @@ module Relaton
10
10
  def initialize # rubocop:disable Lint/MissingSuper
11
11
  @short = :relaton_jis
12
12
  @prefix = "JIS"
13
+ @pubid_identifier = :Jis # Db cache key
13
14
  @defaultprefix = %r{^(JIS|TR)\s}
14
15
  @idtype = "JIS"
15
16
  @datasets = %w[jis-webdesk]