relaton 3.0.3 → 3.0.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/3gpp/data_fetcher.rb +3 -2
  3. data/lib/relaton/bib/CLAUDE.md +24 -4
  4. data/lib/relaton/bib/README.adoc +64 -0
  5. data/lib/relaton/bib/converter/bibxml/from_rfcxml.rb +12 -1
  6. data/lib/relaton/bib/converter/bibxml/to_rfcxml.rb +32 -8
  7. data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb +10 -3
  8. data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb +43 -0
  9. data/lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb +416 -0
  10. data/lib/relaton/bib/converter/bibxml.rb +21 -9
  11. data/lib/relaton/bib/item_data.rb +19 -7
  12. data/lib/relaton/bib/model/abstract.rb +5 -0
  13. data/lib/relaton/bib/model/localized_string.rb +6 -1
  14. data/lib/relaton/bib/sanitizer.rb +20 -5
  15. data/lib/relaton/bipm/data_outcomes_parser.rb +2 -1
  16. data/lib/relaton/bipm/si_brochure_parser.rb +3 -2
  17. data/lib/relaton/core/data_fetcher.rb +3 -2
  18. data/lib/relaton/core/processor.rb +29 -0
  19. data/lib/relaton/db/CLAUDE.md +14 -2
  20. data/lib/relaton/db/cache.rb +5 -2
  21. data/lib/relaton/db.rb +7 -3
  22. data/lib/relaton/ecma/processor.rb +8 -0
  23. data/lib/relaton/equivalence.rb +2 -2
  24. data/lib/relaton/gb/scraper.rb +2 -2
  25. data/lib/relaton/ieee/data_fetcher.rb +2 -1
  26. data/lib/relaton/ietf/bibxml_parser.rb +6 -0
  27. data/lib/relaton/iho/CLAUDE.md +4 -2
  28. data/lib/relaton/iho/bibliography.rb +44 -13
  29. data/lib/relaton/iho/processor.rb +6 -0
  30. data/lib/relaton/index/file_io.rb +9 -8
  31. data/lib/relaton/index/shard_source.rb +3 -2
  32. data/lib/relaton/iso/hit_collection.rb +20 -3
  33. data/lib/relaton/jcgm/meetings_parser.rb +2 -2
  34. data/lib/relaton/nist/cswp.rb +2 -2
  35. data/lib/relaton/nist/data_fetcher.rb +3 -2
  36. data/lib/relaton/sdo/store.rb +3 -1
  37. data/lib/relaton/version.rb +1 -1
  38. data/lib/relaton/xsf/hit.rb +2 -1
  39. metadata +10 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a774b326943089257e3aef6e5402fc1e2d53d4ff557e8b19f845a62d64b9a99b
4
- data.tar.gz: 665fa378d2380f79a4e2952df1ff3d0f7e1de6d91ead262d75261555a4170e5c
3
+ metadata.gz: 6e8ce7b77ba7b71f2bc7232319b926e70116ca3cdadf53395ef39e1dc4437a64
4
+ data.tar.gz: d0f29e54fa2129e444819703dfbd9b20ced132b34613053b6944b560370f9447
5
5
  SHA512:
6
- metadata.gz: bfa2af357a4e288f0ce97fb82ff78ac58b39bc8f8fc8659584ebb29a4568a50175137b8b8cd32b6710aff65b8413cb4ab0d1ea674918187edd9a74592e331a6c
7
- data.tar.gz: e2ad6a9822a6ffa5f9a625bb4095260b81b3906fcff8ba0cf4fbdcea0d543f9e2b14133c7dd29dad0f165c26713b16fc0f06ebfc06d134384d4d0f280ee873b1
6
+ metadata.gz: 158c8d47b5a66ddd647f568aa35327ec4f1ee51e5d4ece559c77a610d3ac9474bcd90aa9bcedd544dd0cdaacf7dcac74f72560f810e014c2caa86477d7e33954
7
+ data.tar.gz: bf49242892015cae4f95d8b63120aa494bb936db2dd98eac34ac8f90ca1f94b889f7d99fd79cc933777f5a04796cb48dc3b933a110f79e29ce6d1f283bb5b744
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "fileutils"
2
3
  require "net/http"
3
4
  require "uri"
@@ -64,7 +65,7 @@ module Relaton
64
65
  # @return [String, nil] file name
65
66
  #
66
67
  def get_file(renewal) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
67
- @current = YAML.load_file CURRENT if File.exist? CURRENT
68
+ @current = Yeptris::Psych.load_file CURRENT if File.exist? CURRENT
68
69
  @current ||= {}
69
70
  uri = URI(CSV_URL)
70
71
  n = 0
@@ -183,7 +184,7 @@ module Relaton
183
184
  # @return [Relaton3gpp::BibliographicItem, nil] merged bibliographic item or nil if no merge has been done
184
185
  #
185
186
  def merge_duplication(bib, file)
186
- hash = YAML.load_file file
187
+ hash = Yeptris::Psych.load_file file
187
188
  existed = Item.from_hash hash
188
189
  changed = update_source? bib, existed
189
190
  bib1, bib2, chng = transposed_relation bib, existed
@@ -61,7 +61,15 @@ Each bibliographic attribute has its own class in `lib/relaton/bib/model/`:
61
61
  **`Relaton::Bib::Sanitizer`** ([lib/relaton/bib/sanitizer.rb](lib/relaton/bib/sanitizer.rb))
62
62
  strips markup outside the basicdoc set. `LocalizedMarkedUpString#content=` calls it on
63
63
  assignment, so every `Title`, `Abstract`, `Note`, `Formattedref` and `Docidentifier` in
64
- every flavor passes through it. Two properties are load-bearing:
64
+ every flavor passes through it. The set it keeps depends on the field, through
65
+ `#sanitizer_allowed`: every field keeps `ALLOWED` (basicdoc inline markup), and `Abstract`
66
+ keeps `ALLOWED_WITH_BLOCKS`, which adds `BLOCK` — the basicdoc lists, notes, tables, figures
67
+ and admonitions, with the children their grammar requires (`name`, `thead`/`tbody`/`tfoot`,
68
+ `image`). That split follows `biblio.rng`: `<abstract>` takes `BasicBlockNoId`, and a title,
69
+ note, docidentifier or formattedref takes only `PureTextElement`, so do not widen the
70
+ default. Before relaton#421 an abstract's list or note unwrapped to bare `<p>`s with no
71
+ error. A consumer must therefore not assume that an abstract holds only `<p>` and inline
72
+ markup. Two properties are load-bearing:
65
73
 
66
74
  - **It does not give up on an undeclared namespace prefix.** Third-party markup prefixes
67
75
  its elements — Crossref returns JATS as `<jats:p><jats:italic>x</jats:italic></jats:p>`,
@@ -99,7 +107,17 @@ every flavor passes through it. Two properties are load-bearing:
99
107
  ### Rendering
100
108
 
101
109
  - **`Renderer::BibtexBuilder`** - Converts `ItemData` to BibTeX format
102
- - **`Converter::BibXml`** - Converts `ItemData` to RFC XML format via `Relaton::Bib::Converter::BibXml.to_xml`
110
+ - **`Converter::BibXml`** - Converts `ItemData` to an `Rfcxml::V3` model object via
111
+ `Relaton::Bib::Converter::BibXml.from_item`; call `to_xml` on the result (this is what
112
+ `ItemData#to_rfcxml` does). Two output shapes share one class hierarchy:
113
+ - `ToRfcxml` / `ToRfcxmlReferencegroup` (default) emit **BibXML**, the shape the Relaton
114
+ data repositories publish. It keeps the RFC 7991-deprecated `<format>` element, so it
115
+ must stay byte-compatible — every relaton data fetcher's `"bibxml"` format is
116
+ `entry.to_rfcxml`. Do not change its output.
117
+ - `ToRfcxmlV3` / `ToRfcxmlReferencegroupV3` (`v3: true`) subclass those and emit strict
118
+ **RFC 7991**: no `<format>`, plus `<stream>`, `ascii*` attributes and `<refcontent>`.
119
+ New v3-only behaviour belongs in the subclass, so the default path stays untouched by
120
+ construction and `spec/bib/relaton/bib/converter/bibxml_spec.rb` keeps passing unedited.
103
121
  - **lutaml-model native** - `to_xml`, `to_yaml`, `to_json` via the serialization classes
104
122
 
105
123
  ### Usage Pattern
@@ -120,11 +138,13 @@ item.to_xml # as <bibitem>
120
138
  item.to_xml(bibdata: true) # as <bibdata>
121
139
  item.to_yaml
122
140
  item.to_bibtex
123
- item.to_rfcxml
141
+ item.to_rfcxml # BibXML (what the data repos publish)
142
+ item.to_rfcxml(v3: true) # strict RFC 7991 / xml2rfc v3
143
+ item.to_rfcxml(anchor: "ISO712", include_keywords: false)
124
144
  ```
125
145
 
126
146
  ## Code Style
127
147
 
128
148
  - Follows Ribose OSS Ruby style guide (inherited via `.rubocop.yml`)
129
- - Target Ruby version: 3.1+
149
+ - Target Ruby version: 3.3+ (`required_ruby_version` in the gemspec, `TargetRubyVersion` in `.rubocop.yml`)
130
150
  - Uses YARD documentation comments
@@ -311,6 +311,70 @@ item.to_rfcxml
311
311
  </reference>
312
312
  ----
313
313
 
314
+ === Export bibliographic item to strict RFC XML v3
315
+
316
+ `to_rfcxml` emits the BibXML shape published by the Relaton data repositories,
317
+ which still carries the `<format>` element that RFC 7991 deprecates. Pass
318
+ `v3: true` for output that follows the RFC 7991 (xml2rfc v3) vocabulary
319
+ instead: no `<format>`, a `<stream>` element, `ascii*` attributes, and
320
+ `<refcontent>` for identifiers that cannot be expressed as `<seriesInfo>`.
321
+
322
+ [source,ruby]
323
+ ----
324
+ item.to_rfcxml v3: true
325
+ <reference anchor="ISO20483">
326
+ <front>
327
+ <title>Cereals and pulses</title>
328
+ <author initials="Ö." asciiInitials="O." surname="Nürk" asciiSurname="Nurk"/>
329
+ <author>
330
+ <organization abbrev="ISO" ascii="International Organization for Standardization">International Organization for Standardization</organization>
331
+ </author>
332
+ <date year="2013"/>
333
+ </front>
334
+ <refcontent>ISO 20483:2013-2014</refcontent>
335
+ </reference>
336
+ ----
337
+
338
+ Documents published by the IETF (identified by a contributing organization
339
+ named `IETF`, `Internet Engineering Task Force` or `RFC Publisher`) express
340
+ their identifiers as `<seriesInfo>` rather than `<refcontent>`:
341
+
342
+ [source,ruby]
343
+ ----
344
+ <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
345
+ <stream>IETF</stream>
346
+ <front>
347
+ <title>Key words for use in RFCs to Indicate Requirement Levels</title>
348
+ <seriesInfo name="BCP" value="14"/>
349
+ <seriesInfo name="RFC" value="2119"/>
350
+ ...
351
+ </front>
352
+ </reference>
353
+ ----
354
+
355
+ The v3 mode also applies the citation rules the IETF toolchain expects, which
356
+ the BibXML mode leaves alone:
357
+
358
+ * Contributors with role `author` or `editor` become `<author>`; the wider set
359
+ (`publisher`, `authorizer`, `translator`, …) is used only when the item names
360
+ no author or editor, so an RFC's publisher is not listed as its author.
361
+ * `<title>` takes the composite `title[@type="main"]`, else joins
362
+ title-intro / title-main / title-part, so multipart standards keep their full
363
+ title. Inline markup is flattened, since v3 `<title>` is text-only.
364
+ * Identifiers that restate the reference label, `URN` identifiers and the
365
+ `I-D.` anchor form are left out of `<refcontent>`; Internet-Draft
366
+ `<seriesInfo>` carries the full `draft-…` name, which is what xml2rfc
367
+ resolves against.
368
+ * Where folding a name leaves no letters (a non-Latin script), the `ascii*`
369
+ attribute is omitted rather than emitted blank.
370
+
371
+ Two further options are accepted by both modes:
372
+
373
+ `anchor:`:: overrides the anchor otherwise derived from `docnumber`, for
374
+ callers that carry their own identifier (for example a Metanorma Semantic XML
375
+ `bibitem/@id`).
376
+ `include_keywords:`:: set to `false` to suppress `<keyword>` elements.
377
+
314
378
  === Logging
315
379
 
316
380
  RelatonBib uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
@@ -326,7 +326,18 @@ module Relaton
326
326
 
327
327
  t = Title.new(content: si.name, language: "en", script: "Latn")
328
328
  Series.new(title: [t], number: si.value, type: "main")
329
- end.compact
329
+ end.compact + stream_series
330
+ end
331
+
332
+ # RFC XML v3 carries the publication stream in a <stream> element.
333
+ # ItemData has no dedicated slot for it, so it is kept the way
334
+ # relaton-ietf does: as a series marked `type="stream"`.
335
+ def stream_series
336
+ return [] unless @reference.stream
337
+
338
+ t = Title.new(content: @reference.stream, language: "en",
339
+ script: "Latn")
340
+ [Series.new(title: [t], type: "stream")]
330
341
  end
331
342
 
332
343
  # --- Keyword ---
@@ -5,14 +5,15 @@ module Relaton
5
5
  module Converter
6
6
  module BibXml
7
7
  class ToRfcxml
8
- def initialize(item, include_keywords: true)
8
+ def initialize(item, include_keywords: true, anchor: nil)
9
9
  @item = item
10
10
  @include_keywords = include_keywords
11
+ @anchor = anchor
11
12
  end
12
13
 
13
14
  def transform
14
15
  model = ::Rfcxml::V3::Reference.new
15
- model.anchor = @item.docnumber || derive_anchor
16
+ model.anchor = @anchor || @item.docnumber || derive_anchor
16
17
  model.target = create_target
17
18
  model.front = create_front
18
19
  model.format = create_format
@@ -26,14 +27,22 @@ module Relaton
26
27
  di&.content&.to_s&.gsub(" ", ".")
27
28
  end
28
29
 
30
+ # A <uri> may legitimately carry no type -- biblio.rng makes it
31
+ # optional, and hand-authored bibitems often omit it -- so never call
32
+ # String methods on it unguarded.
33
+ def target_types = %w[src doi]
34
+
29
35
  def create_target
30
- target = @item.source.detect { |l| l.type.casecmp("src").zero? } ||
31
- @item.source.detect { |l| l.type.casecmp("doi").zero? }
36
+ target = target_types.filter_map { |t| source_of_type(t) }.first
32
37
  return unless target
33
38
 
34
39
  target.content.to_s
35
40
  end
36
41
 
42
+ def source_of_type(type)
43
+ @item.source.detect { |l| l.type&.casecmp(type)&.zero? }
44
+ end
45
+
37
46
  def create_front # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
38
47
  front = ::Rfcxml::V3::Front.new
39
48
  front.title = Rfcxml::V3::Title.new(content: @item.title[0].content) if @item.title.any?
@@ -59,9 +68,16 @@ module Relaton
59
68
  end
60
69
  end
61
70
 
71
+ # A `type="stream"` series is the publication stream, not a series:
72
+ # it has no number, so it would yield a valueless <seriesInfo>.
73
+ def seriesinfo_series?(ser)
74
+ ser.type != "stream" &&
75
+ ser.title.reject { |t| t.content == "DOI" }.any?
76
+ end
77
+
62
78
  def series_to_seriesinfo # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
63
79
  @item.series.select do |s|
64
- s.title.reject { |t| t.content == "DOI" }.any?
80
+ seriesinfo_series?(s)
65
81
  end.uniq do |s|
66
82
  s.title.find { |t| t.content != "DOI" }.content
67
83
  end.each_with_object([]) do |s, si|
@@ -71,8 +87,13 @@ module Relaton
71
87
  end
72
88
  end
73
89
 
90
+ # Which contributors become <author>; overridden by the v3 emitter.
91
+ def contributors_for_authors
92
+ @item.contributor.reject { |c| committee_contributor?(c) }
93
+ end
94
+
74
95
  def create_authors # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
75
- @item.contributor.reject { |c| committee_contributor?(c) }.map do |contrib|
96
+ contributors_for_authors.map do |contrib|
76
97
  role = "editor" if contrib.role.detect { |r| r.type == "editor" }
77
98
  Rfcxml::V3::Author.new(
78
99
  role: role,
@@ -284,7 +305,10 @@ module Relaton
284
305
  return unless @item.abstract.any?
285
306
 
286
307
  content = @item.abstract[0].content
287
- paragraphs = content.scan(%r{<p>(.*?)</p>}m).flatten
308
+ # Semantic XML paragraphs carry attributes (`<p id="_349ea…">`);
309
+ # match those too, or the whole abstract falls through to the
310
+ # fallback below and the markup is escaped into the <t>.
311
+ paragraphs = content.scan(%r{<p\b[^>]*>(.*?)</p>}m).flatten
288
312
  paragraphs = [content] if paragraphs.empty?
289
313
  ts = paragraphs.map { |p| Rfcxml::V3::Text.new(content: CGI.unescapeHTML(p)) }
290
314
  Rfcxml::V3::Abstract.new(t: ts)
@@ -294,7 +318,7 @@ module Relaton
294
318
 
295
319
  def create_format # rubocop:disable Metrics/AbcSize
296
320
  @item.source.each_with_object([]) do |l, a|
297
- next unless FORMAT_TYPES.any? { |ft| l.type.casecmp(ft).zero? }
321
+ next unless FORMAT_TYPES.any? { |ft| l.type&.casecmp(ft)&.zero? }
298
322
 
299
323
  a << Rfcxml::V3::Format.new(
300
324
  type: l.type, target: l.content,
@@ -3,9 +3,10 @@ module Relaton
3
3
  module Converter
4
4
  module BibXml
5
5
  class ToRfcxmlReferencegroup
6
- def initialize(item, include_keywords: true)
6
+ def initialize(item, include_keywords: true, anchor: nil)
7
7
  @item = item
8
8
  @include_keywords = include_keywords
9
+ @anchor = anchor
9
10
  end
10
11
 
11
12
  def transform
@@ -30,6 +31,8 @@ module Relaton
30
31
  end
31
32
 
32
33
  def create_anchor
34
+ return @anchor if @anchor
35
+
33
36
  docid = @item.docidentifier.detect(&:primary) ||
34
37
  @item.docidentifier[0]
35
38
  return unless docid
@@ -38,9 +41,13 @@ module Relaton
38
41
  id.sub(/^(RFC|BCP|FYI|STD) /, '\1').sub(/^\w+\./, "")
39
42
  end
40
43
 
44
+ # An untyped <uri> is legal, so guard before comparing.
45
+ def target_types = %w[src doi]
46
+
41
47
  def create_target
42
- target = @item.source.detect { |l| l.type.casecmp("src").zero? } ||
43
- @item.source.detect { |l| l.type.casecmp("doi").zero? }
48
+ target = target_types.filter_map do |type|
49
+ @item.source.detect { |l| l.type&.casecmp(type)&.zero? }
50
+ end.first
44
51
  return unless target
45
52
 
46
53
  target.content.to_s
@@ -0,0 +1,43 @@
1
+ module Relaton
2
+ module Bib
3
+ module Converter
4
+ module BibXml
5
+ #
6
+ # Strict RFC 7991 flavour of {ToRfcxmlReferencegroup}: members are
7
+ # rendered by {ToRfcxmlV3}, and every one of them gets an anchor,
8
+ # which the v3 grammar makes mandatory on `<reference>`.
9
+ #
10
+ class ToRfcxmlReferencegroupV3 < ToRfcxmlReferencegroup
11
+ private
12
+
13
+ def target_types = %w[src HTML doi]
14
+
15
+ # <referencegroup> needs an anchor just as much as its members do,
16
+ # so fall back past the docidentifier the parent relies on.
17
+ def create_anchor
18
+ anchor = super || @item.docnumber || @item.id
19
+ anchor && anchor.to_s.strip.gsub(/\s+/, ".")
20
+ end
21
+
22
+ def build_references
23
+ included_items.each_with_index.map do |item, idx|
24
+ ref = ToRfcxmlV3.new(item, include_keywords: @include_keywords)
25
+ .transform
26
+ ref.anchor = member_anchor(idx) if ref.anchor.to_s.empty?
27
+ ref
28
+ end
29
+ end
30
+
31
+ def included_items
32
+ @item.relation.select { |rel| rel.type == "includes" }
33
+ .map(&:bibitem)
34
+ end
35
+
36
+ def member_anchor(idx)
37
+ "#{create_anchor || 'reference'}_#{idx + 1}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end