relaton-bib 2.1.7 → 2.1.8
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 +15 -3
- data/README.adoc +64 -0
- data/lib/relaton/bib/converter/bibxml/from_rfcxml.rb +12 -1
- data/lib/relaton/bib/converter/bibxml/to_rfcxml.rb +32 -8
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb +10 -3
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb +43 -0
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb +416 -0
- data/lib/relaton/bib/converter/bibxml.rb +21 -9
- data/lib/relaton/bib/item_data.rb +19 -7
- data/lib/relaton/bib/model/series.rb +3 -1
- data/lib/relaton/bib/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 395f67780defebb9f033822f67f28dec65f7a507b687631d718e7a143926bf95
|
|
4
|
+
data.tar.gz: 15b9c502271a17e795f21886d6721cff6533a3a9c9e67aaec8ef9b332ddb87de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71e9495276f45dab74db3170a882d8db0a62aff68c29f5daa00e1c59abac9ad6d4069b887da6aee1293b6df1b3e2e57878c8abd9158014eac9d945f92a478f4b
|
|
7
|
+
data.tar.gz: 6045720a0265a3fb0c54ae76b7870a8b4288ef310832f9519bb488bf18b3f5cce94c00e08bb5b6ae42c832dcb3124a99de6a8e80599aa78ab90b779963990c42
|
data/CLAUDE.md
CHANGED
|
@@ -77,7 +77,17 @@ Each bibliographic attribute has its own class in `lib/relaton/bib/model/`:
|
|
|
77
77
|
### Rendering
|
|
78
78
|
|
|
79
79
|
- **`Renderer::BibtexBuilder`** - Converts `ItemData` to BibTeX format
|
|
80
|
-
- **`Converter::BibXml`** - Converts `ItemData` to
|
|
80
|
+
- **`Converter::BibXml`** - Converts `ItemData` to an `Rfcxml::V3` model object via
|
|
81
|
+
`Relaton::Bib::Converter::BibXml.from_item`; call `to_xml` on the result (this is what
|
|
82
|
+
`ItemData#to_rfcxml` does). Two output shapes share one class hierarchy:
|
|
83
|
+
- `ToRfcxml` / `ToRfcxmlReferencegroup` (default) emit **BibXML**, the shape the Relaton
|
|
84
|
+
data repositories publish. It keeps the RFC 7991-deprecated `<format>` element, so it
|
|
85
|
+
must stay byte-compatible — every relaton data fetcher's `"bibxml"` format is
|
|
86
|
+
`entry.to_rfcxml`. Do not change its output.
|
|
87
|
+
- `ToRfcxmlV3` / `ToRfcxmlReferencegroupV3` (`v3: true`) subclass those and emit strict
|
|
88
|
+
**RFC 7991**: no `<format>`, plus `<stream>`, `ascii*` attributes and `<refcontent>`.
|
|
89
|
+
New v3-only behaviour belongs in the subclass, so the default path stays untouched by
|
|
90
|
+
construction and `spec/relaton/bib/converter/bibxml_spec.rb` keeps passing unedited.
|
|
81
91
|
- **lutaml-model native** - `to_xml`, `to_yaml`, `to_json` via the serialization classes
|
|
82
92
|
|
|
83
93
|
### Usage Pattern
|
|
@@ -98,11 +108,13 @@ item.to_xml # as <bibitem>
|
|
|
98
108
|
item.to_xml(bibdata: true) # as <bibdata>
|
|
99
109
|
item.to_yaml
|
|
100
110
|
item.to_bibtex
|
|
101
|
-
item.to_rfcxml
|
|
111
|
+
item.to_rfcxml # BibXML (what the data repos publish)
|
|
112
|
+
item.to_rfcxml(v3: true) # strict RFC 7991 / xml2rfc v3
|
|
113
|
+
item.to_rfcxml(anchor: "ISO712", include_keywords: false)
|
|
102
114
|
```
|
|
103
115
|
|
|
104
116
|
## Code Style
|
|
105
117
|
|
|
106
118
|
- Follows Ribose OSS Ruby style guide (inherited via `.rubocop.yml`)
|
|
107
|
-
- Target Ruby version: 3.
|
|
119
|
+
- Target Ruby version: 3.2+ (`required_ruby_version` in the gemspec, `TargetRubyVersion` in `.rubocop.yml`)
|
|
108
120
|
- Uses YARD documentation comments
|
data/README.adoc
CHANGED
|
@@ -334,6 +334,70 @@ item.to_rfcxml
|
|
|
334
334
|
</reference>
|
|
335
335
|
----
|
|
336
336
|
|
|
337
|
+
=== Export bibliographic item to strict RFC XML v3
|
|
338
|
+
|
|
339
|
+
`to_rfcxml` emits the BibXML shape published by the Relaton data repositories,
|
|
340
|
+
which still carries the `<format>` element that RFC 7991 deprecates. Pass
|
|
341
|
+
`v3: true` for output that follows the RFC 7991 (xml2rfc v3) vocabulary
|
|
342
|
+
instead: no `<format>`, a `<stream>` element, `ascii*` attributes, and
|
|
343
|
+
`<refcontent>` for identifiers that cannot be expressed as `<seriesInfo>`.
|
|
344
|
+
|
|
345
|
+
[source,ruby]
|
|
346
|
+
----
|
|
347
|
+
item.to_rfcxml v3: true
|
|
348
|
+
<reference anchor="ISO20483">
|
|
349
|
+
<front>
|
|
350
|
+
<title>Cereals and pulses</title>
|
|
351
|
+
<author initials="Ö." asciiInitials="O." surname="Nürk" asciiSurname="Nurk"/>
|
|
352
|
+
<author>
|
|
353
|
+
<organization abbrev="ISO" ascii="International Organization for Standardization">International Organization for Standardization</organization>
|
|
354
|
+
</author>
|
|
355
|
+
<date year="2013"/>
|
|
356
|
+
</front>
|
|
357
|
+
<refcontent>ISO 20483:2013-2014</refcontent>
|
|
358
|
+
</reference>
|
|
359
|
+
----
|
|
360
|
+
|
|
361
|
+
Documents published by the IETF (identified by a contributing organization
|
|
362
|
+
named `IETF`, `Internet Engineering Task Force` or `RFC Publisher`) express
|
|
363
|
+
their identifiers as `<seriesInfo>` rather than `<refcontent>`:
|
|
364
|
+
|
|
365
|
+
[source,ruby]
|
|
366
|
+
----
|
|
367
|
+
<reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
|
|
368
|
+
<stream>IETF</stream>
|
|
369
|
+
<front>
|
|
370
|
+
<title>Key words for use in RFCs to Indicate Requirement Levels</title>
|
|
371
|
+
<seriesInfo name="BCP" value="14"/>
|
|
372
|
+
<seriesInfo name="RFC" value="2119"/>
|
|
373
|
+
...
|
|
374
|
+
</front>
|
|
375
|
+
</reference>
|
|
376
|
+
----
|
|
377
|
+
|
|
378
|
+
The v3 mode also applies the citation rules the IETF toolchain expects, which
|
|
379
|
+
the BibXML mode leaves alone:
|
|
380
|
+
|
|
381
|
+
* Contributors with role `author` or `editor` become `<author>`; the wider set
|
|
382
|
+
(`publisher`, `authorizer`, `translator`, …) is used only when the item names
|
|
383
|
+
no author or editor, so an RFC's publisher is not listed as its author.
|
|
384
|
+
* `<title>` takes the composite `title[@type="main"]`, else joins
|
|
385
|
+
title-intro / title-main / title-part, so multipart standards keep their full
|
|
386
|
+
title. Inline markup is flattened, since v3 `<title>` is text-only.
|
|
387
|
+
* Identifiers that restate the reference label, `URN` identifiers and the
|
|
388
|
+
`I-D.` anchor form are left out of `<refcontent>`; Internet-Draft
|
|
389
|
+
`<seriesInfo>` carries the full `draft-…` name, which is what xml2rfc
|
|
390
|
+
resolves against.
|
|
391
|
+
* Where folding a name leaves no letters (a non-Latin script), the `ascii*`
|
|
392
|
+
attribute is omitted rather than emitted blank.
|
|
393
|
+
|
|
394
|
+
Two further options are accepted by both modes:
|
|
395
|
+
|
|
396
|
+
`anchor:`:: overrides the anchor otherwise derived from `docnumber`, for
|
|
397
|
+
callers that carry their own identifier (for example a Metanorma Semantic XML
|
|
398
|
+
`bibitem/@id`).
|
|
399
|
+
`include_keywords:`:: set to `false` to suppress `<keyword>` elements.
|
|
400
|
+
|
|
337
401
|
=== Logging
|
|
338
402
|
|
|
339
403
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
43
|
-
@item.source.detect { |l| l.type
|
|
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
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module Bib
|
|
3
|
+
module Converter
|
|
4
|
+
module BibXml
|
|
5
|
+
#
|
|
6
|
+
# Strict RFC 7991 (xml2rfc v3) flavour of {ToRfcxml}.
|
|
7
|
+
#
|
|
8
|
+
# It differs from the BibXML output its parent produces in four ways:
|
|
9
|
+
#
|
|
10
|
+
# * the deprecated `<format>` element is never emitted;
|
|
11
|
+
# * identifiers of documents published outside the IETF go into
|
|
12
|
+
# `<refcontent>` rather than being forced into `<seriesInfo>`
|
|
13
|
+
# (RFC 7991 2.39), as do series that carry no number;
|
|
14
|
+
# * `<stream>` and the `ascii*` attributes are populated;
|
|
15
|
+
# * `<front>` is filled out enough to satisfy the v3 grammar, which
|
|
16
|
+
# requires a title and at least one author.
|
|
17
|
+
#
|
|
18
|
+
class ToRfcxmlV3 < ToRfcxml
|
|
19
|
+
# Organizations whose documents are "home" standards: their
|
|
20
|
+
# identifiers are expressible as <seriesInfo>.
|
|
21
|
+
HOME_ORGS = [
|
|
22
|
+
"IETF", "Internet Engineering Task Force", "RFC Publisher"
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Identifier types that are already emitted as <seriesInfo> by the
|
|
26
|
+
# parent, or that never belong in one.
|
|
27
|
+
NON_AUTHORITATIVE_TYPES = %w[DOI URI].freeze
|
|
28
|
+
|
|
29
|
+
# Identifiers metanorma-ietf drops as internal bookkeeping.
|
|
30
|
+
IGNORED_ID_RE = /\A(rfc-anchor|Internet-Draft)/
|
|
31
|
+
|
|
32
|
+
# Series that name the document itself; handled as identifiers.
|
|
33
|
+
SUBSERIES = %w[BCP STD].freeze
|
|
34
|
+
|
|
35
|
+
HOME_ID_RE = /\A(?<name>RFC|BCP|STD|FYI)\s*(?<value>\d+)\z/i
|
|
36
|
+
# xml2rfc only resolves a draft reference when seriesInfo carries the
|
|
37
|
+
# full draft name, so keep `draft-…` intact. The `I-D.` anchor form
|
|
38
|
+
# is a citation label, not the identifier (metanorma-ietf#283).
|
|
39
|
+
DRAFT_ID_RE = /\A(?<value>draft-\S+)\z/i
|
|
40
|
+
DRAFT_ANCHOR_RE = /\AI-D[.\s]\s*\S+\z/i
|
|
41
|
+
|
|
42
|
+
# Identifier types that duplicate a human-readable identifier or are
|
|
43
|
+
# bookkeeping rather than citation content (metanorma-ietf#301).
|
|
44
|
+
EXCLUDED_ID_TYPES = %w[URN].freeze
|
|
45
|
+
|
|
46
|
+
# Title parts joined when relaton supplies no composite title.
|
|
47
|
+
TITLE_PARTS = %w[title-intro title-main title-part].freeze
|
|
48
|
+
|
|
49
|
+
# Roles that may stand in for an author when the item names none,
|
|
50
|
+
# mirroring the released renderer's `creatornames_roles_allowed`.
|
|
51
|
+
FALLBACK_ROLES = %w[performer adapter translator publisher
|
|
52
|
+
distributor authorizer].freeze
|
|
53
|
+
|
|
54
|
+
# The values RFC XML v3 allows on <stream>; "editorial" was added by
|
|
55
|
+
# RFC 9280. Keyed by the spellings the RFC Editor and relaton-ietf
|
|
56
|
+
# use, so "Independent Submission" is not silently thrown away.
|
|
57
|
+
STREAMS = {
|
|
58
|
+
"ietf" => "IETF", "iab" => "IAB", "irtf" => "IRTF",
|
|
59
|
+
"independent" => "independent",
|
|
60
|
+
"independent submission" => "independent",
|
|
61
|
+
"independent submission stream" => "independent",
|
|
62
|
+
"editorial" => "editorial", "rfc editor" => "editorial",
|
|
63
|
+
"rfc-editor" => "editorial"
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
# Latin letters with no NFKD decomposition, which would otherwise be
|
|
67
|
+
# dropped outright ("Sørensen" -> "Srensen").
|
|
68
|
+
TRANSLITERATIONS = {
|
|
69
|
+
"æ" => "ae", "Æ" => "AE", "œ" => "oe", "Œ" => "OE",
|
|
70
|
+
"ø" => "o", "Ø" => "O", "ß" => "ss", "ẞ" => "SS",
|
|
71
|
+
"đ" => "d", "Đ" => "D", "ð" => "d", "Ð" => "D",
|
|
72
|
+
"þ" => "th", "Þ" => "Th", "ł" => "l", "Ł" => "L",
|
|
73
|
+
"ħ" => "h", "Ħ" => "H", "ŋ" => "ng", "Ŋ" => "NG",
|
|
74
|
+
"ŧ" => "t", "Ŧ" => "T", "ı" => "i", "ĸ" => "k"
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
def transform
|
|
78
|
+
model = ::Rfcxml::V3::Reference.new
|
|
79
|
+
model.anchor = create_anchor
|
|
80
|
+
model.target = create_target
|
|
81
|
+
model.stream = create_stream
|
|
82
|
+
model.front = create_front
|
|
83
|
+
model.refcontent = create_refcontent
|
|
84
|
+
model
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# The released IETF renderer takes an HTML-typed uri as the reference
|
|
90
|
+
# target when there is no src (isodoc/ietf/references.rb), and the
|
|
91
|
+
# transformer does the same; match it here.
|
|
92
|
+
def target_types = %w[src HTML doi]
|
|
93
|
+
|
|
94
|
+
# anchor is mandatory in v3 and is an XML identifier, so fall back
|
|
95
|
+
# through every source the item has and never leave whitespace in it.
|
|
96
|
+
def create_anchor
|
|
97
|
+
sanitize_anchor(@anchor || @item.docnumber || derive_anchor ||
|
|
98
|
+
@item.id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sanitize_anchor(anchor)
|
|
102
|
+
return nil if anchor.nil?
|
|
103
|
+
|
|
104
|
+
anchor.to_s.strip.gsub(/\s+/, ".")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# <front> needs a title and at least one author to be valid v3.
|
|
108
|
+
def create_front
|
|
109
|
+
front = super
|
|
110
|
+
front.title = compound_title || formattedref_title
|
|
111
|
+
front.author = [unknown_author] if Array(front.author).empty?
|
|
112
|
+
front
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The base emitter takes title[0], which on a multipart standard is
|
|
116
|
+
# the intro alone ("IT Security techniques"). A citation needs the
|
|
117
|
+
# whole compound, so prefer the composite title relaton builds, then
|
|
118
|
+
# the intro-main-part join, then whatever is first.
|
|
119
|
+
def compound_title
|
|
120
|
+
content = composite_title || joined_title ||
|
|
121
|
+
@item.title.first&.content
|
|
122
|
+
return nil if content.nil?
|
|
123
|
+
|
|
124
|
+
::Rfcxml::V3::Title.new(content: plain_text(content))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def composite_title
|
|
128
|
+
@item.title.find { |t| t.type == "main" }&.content
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def joined_title
|
|
132
|
+
parts = TITLE_PARTS.filter_map do |type|
|
|
133
|
+
@item.title.find { |t| t.type == type }&.content
|
|
134
|
+
end
|
|
135
|
+
parts.empty? ? nil : parts.join(" - ")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def formattedref_title
|
|
139
|
+
content = @item.formattedref&.content or return nil
|
|
140
|
+
|
|
141
|
+
::Rfcxml::V3::Title.new(content: plain_text(content))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# v3 <title> is text-only, so inline markup a formattedref or title
|
|
145
|
+
# carries has to be flattened rather than escaped into the output.
|
|
146
|
+
def plain_text(content)
|
|
147
|
+
text = content.to_s
|
|
148
|
+
return text unless text.include?("<")
|
|
149
|
+
|
|
150
|
+
Nokogiri::XML.fragment(text).text.squeeze(" ").strip
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def unknown_author
|
|
154
|
+
::Rfcxml::V3::Author.new(surname: "Unknown")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# One <seriesInfo> per (name, value): the same identifier can arrive
|
|
158
|
+
# from a docidentifier and a series, or from two docidentifiers that
|
|
159
|
+
# spell it differently ("RFC 2119" and "RFC2119").
|
|
160
|
+
def create_seriesinfo
|
|
161
|
+
(docidentifier_to_seriesinfo + series_to_seriesinfo +
|
|
162
|
+
identifier_to_seriesinfo).uniq { |si| [si.name, si.value] }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Only series that carry a number can become <seriesInfo>; the rest
|
|
166
|
+
# are rendered as <refcontent>.
|
|
167
|
+
def series_to_seriesinfo
|
|
168
|
+
numbered_series.map do |ser|
|
|
169
|
+
::Rfcxml::V3::SeriesInfo.new(name: series_title(ser).content,
|
|
170
|
+
value: ser.number)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def numbered_series
|
|
175
|
+
renderable_series.select(&:number)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Series that are neither the stream marker nor a restatement of
|
|
179
|
+
# something already rendered from the document's identifiers. Only
|
|
180
|
+
# names actually emitted elsewhere are dropped: a series such as
|
|
181
|
+
# Internet-Draft still carries its number when no docidentifier
|
|
182
|
+
# supplies one.
|
|
183
|
+
def renderable_series
|
|
184
|
+
@item.series.reject { |ser| skip_series?(ser) }
|
|
185
|
+
.uniq { |ser| series_title(ser).content }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def skip_series?(ser)
|
|
189
|
+
return true if ser.type == "stream"
|
|
190
|
+
|
|
191
|
+
title = series_title(ser)
|
|
192
|
+
title.nil? || title.content.to_s == "DOI" ||
|
|
193
|
+
identifier_series_names.include?(title.content.to_s)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Series names already covered by <seriesInfo> or <refcontent> built
|
|
197
|
+
# from the item's identifiers.
|
|
198
|
+
def identifier_series_names
|
|
199
|
+
@identifier_series_names ||=
|
|
200
|
+
identifier_to_seriesinfo.map(&:name) +
|
|
201
|
+
subseries_identifiers.map { |id| id.split.first }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def series_title(ser)
|
|
205
|
+
ser.title.find { |title| title.content.to_s != "DOI" }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# --- Identifiers ---
|
|
209
|
+
|
|
210
|
+
def identifier_to_seriesinfo
|
|
211
|
+
return [] unless home_standard?
|
|
212
|
+
|
|
213
|
+
authoritative_identifiers.filter_map { |id| id_to_seriesinfo(id) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def id_to_seriesinfo(id)
|
|
217
|
+
name, value = split_identifier(id)
|
|
218
|
+
return nil unless name
|
|
219
|
+
|
|
220
|
+
# No `stream` attribute here: in v3 the stream is carried by the
|
|
221
|
+
# <stream> element, and duplicating it only adds noise.
|
|
222
|
+
::Rfcxml::V3::SeriesInfo.new(name: name, value: value,
|
|
223
|
+
status: seriesinfo_status)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def split_identifier(id)
|
|
227
|
+
if (match = DRAFT_ID_RE.match(id))
|
|
228
|
+
["Internet-Draft", match[:value]]
|
|
229
|
+
elsif (match = HOME_ID_RE.match(id))
|
|
230
|
+
[match[:name].upcase, match[:value]]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# An untyped docidentifier that merely restates the reference's own
|
|
235
|
+
# label ("ZELLER", "Grail") is a citation label, not a citation: keep
|
|
236
|
+
# it only when the reference would otherwise show nothing at all.
|
|
237
|
+
def label_echo?(docid)
|
|
238
|
+
return false unless docid.type.nil?
|
|
239
|
+
|
|
240
|
+
docid.content.to_s.casecmp(label_candidates.to_s).zero? &&
|
|
241
|
+
reference_has_visible_text?
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def label_candidates
|
|
245
|
+
@item.docnumber || @item.id
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def reference_has_visible_text?
|
|
249
|
+
@item.title.any? || @item.formattedref ||
|
|
250
|
+
@item.contributor.any? || @item.date.any?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def seriesinfo_status
|
|
254
|
+
@item.status&.stage&.content
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def authoritative_identifiers
|
|
258
|
+
ids = subseries_identifiers + docidentifier_identifiers
|
|
259
|
+
ids.reject { |id| id.empty? || IGNORED_ID_RE.match?(id) }.uniq
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def docidentifier_identifiers
|
|
263
|
+
@item.docidentifier.reject { |di| non_authoritative?(di) }
|
|
264
|
+
.map { |di| di.content.to_s.strip }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def non_authoritative?(docid)
|
|
268
|
+
NON_AUTHORITATIVE_TYPES.include?(docid.type) ||
|
|
269
|
+
EXCLUDED_ID_TYPES.include?(docid.type) ||
|
|
270
|
+
docid.scope == "trademark" ||
|
|
271
|
+
DRAFT_ANCHOR_RE.match?(docid.content.to_s) ||
|
|
272
|
+
label_echo?(docid)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def subseries_identifiers
|
|
276
|
+
@item.series.filter_map do |ser|
|
|
277
|
+
title = ser.title.find { |t| SUBSERIES.include?(t.content.to_s) }
|
|
278
|
+
"#{title.content} #{ser.number}" if title && ser.number
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def home_standard?
|
|
283
|
+
@item.contributor.any? do |contrib|
|
|
284
|
+
org = contrib.organization or next false
|
|
285
|
+
|
|
286
|
+
([org.abbreviation&.content] + org.name.map(&:content))
|
|
287
|
+
.compact.any? { |name| HOME_ORGS.include?(name.to_s) }
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# --- refcontent ---
|
|
292
|
+
|
|
293
|
+
def create_refcontent
|
|
294
|
+
(identifier_refcontent + series_refcontent)
|
|
295
|
+
.map { |text| ::Rfcxml::V3::Refcontent.new(content: text) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Identifiers that cannot be expressed as <seriesInfo> — either
|
|
299
|
+
# because the document is not an IETF one, or because the identifier
|
|
300
|
+
# does not split into a series name and number.
|
|
301
|
+
# Whatever <seriesInfo> already states must not be repeated here,
|
|
302
|
+
# whichever route put it there.
|
|
303
|
+
def identifier_refcontent
|
|
304
|
+
ids = authoritative_identifiers.reject { |id| in_seriesinfo?(id) }
|
|
305
|
+
ids.empty? ? [] : [ids.join(", ")]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def in_seriesinfo?(id)
|
|
309
|
+
name, value = split_identifier(id)
|
|
310
|
+
return false if name.nil?
|
|
311
|
+
|
|
312
|
+
create_seriesinfo.any? { |si| si.name == name && si.value == value }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def series_refcontent
|
|
316
|
+
(renderable_series - numbered_series).filter_map do |ser|
|
|
317
|
+
text = ser.formattedref&.content&.to_s || series_description(ser)
|
|
318
|
+
text unless text.strip.empty?
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def series_description(ser)
|
|
323
|
+
[series_title(ser)&.content, ser.run, ser.organization,
|
|
324
|
+
ser.place&.city, series_dates(ser)]
|
|
325
|
+
.map(&:to_s).reject(&:empty?).join(", ")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def series_dates(ser)
|
|
329
|
+
[ser.from, ser.to].compact.map(&:to_s).reject(&:empty?).join("-")
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# --- stream ---
|
|
333
|
+
|
|
334
|
+
# Values outside the grammar (relaton-ietf also has "Legacy") have no
|
|
335
|
+
# valid representation, so they are dropped with a warning rather
|
|
336
|
+
# than emitted as invalid XML.
|
|
337
|
+
def create_stream
|
|
338
|
+
value = ext_stream || series_stream
|
|
339
|
+
return nil if value.nil?
|
|
340
|
+
|
|
341
|
+
stream = STREAMS[value.to_s.strip.downcase]
|
|
342
|
+
unless stream
|
|
343
|
+
Util.warn "Dropping stream `#{value}`: not an RFC XML v3 stream"
|
|
344
|
+
end
|
|
345
|
+
stream
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def ext_stream
|
|
349
|
+
@item.ext.respond_to?(:stream) ? @item.ext.stream : nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def series_stream
|
|
353
|
+
ser = @item.series.find { |s| s.type == "stream" }
|
|
354
|
+
ser && ser.title.first&.content
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# --- Contributors ---
|
|
358
|
+
|
|
359
|
+
# Every relaton-ietf RFC record carries publisher and authorizer
|
|
360
|
+
# contributors, and published RFC XML never lists those as authors.
|
|
361
|
+
# So: authors and editors if there are any, and only otherwise fall
|
|
362
|
+
# back to the wider set, which keeps a translator-only monograph its
|
|
363
|
+
# translator (metanorma-ietf#301).
|
|
364
|
+
def contributors_for_authors
|
|
365
|
+
contribs = super
|
|
366
|
+
primary = contribs.select { |c| role?(c, %w[author editor]) }
|
|
367
|
+
primary.any? ? primary : contribs.select { |c| fallback_author?(c) }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def role?(contrib, types)
|
|
371
|
+
contrib.role.any? { |r| types.include?(r.type) }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# A contributor with no role at all is an author by default; that is
|
|
375
|
+
# how most non-IETF bibitems are written.
|
|
376
|
+
def fallback_author?(contrib)
|
|
377
|
+
contrib.role.empty? || role?(contrib, FALLBACK_ROLES)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# --- ascii folding ---
|
|
381
|
+
|
|
382
|
+
def create_authors
|
|
383
|
+
super.each do |author|
|
|
384
|
+
author.ascii_fullname = ascii(author.fullname)
|
|
385
|
+
author.ascii_surname = ascii(author.surname)
|
|
386
|
+
author.ascii_initials = ascii(author.initials)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def create_organization(contrib)
|
|
391
|
+
org = super or return nil
|
|
392
|
+
|
|
393
|
+
org.ascii = ascii(Array(org.content).join)
|
|
394
|
+
org
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Transliterate to bare ASCII: expand the letters above, then strip
|
|
398
|
+
# the combining marks NFKD leaves behind. "Nürk" -> "Nurk",
|
|
399
|
+
# "Ö." -> "O.", "Sørensen" -> "Sorensen".
|
|
400
|
+
def ascii(str)
|
|
401
|
+
return nil if str.nil?
|
|
402
|
+
|
|
403
|
+
folded = str.to_s.gsub(/[#{TRANSLITERATIONS.keys.join}]/,
|
|
404
|
+
TRANSLITERATIONS)
|
|
405
|
+
.unicode_normalize(:nfkd)
|
|
406
|
+
.encode("ASCII", invalid: :replace, undef: :replace, replace: "")
|
|
407
|
+
# Folding a non-Latin script leaves only the punctuation and
|
|
408
|
+
# spacing behind ("Νίκος Παπαδόπουλος" -> " "), which is worse than
|
|
409
|
+
# no attribute at all. Real transliteration is the caller's policy.
|
|
410
|
+
folded.match?(/[[:alnum:]]/) ? folded : nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require_relative "bibxml/to_rfcxml"
|
|
2
2
|
require_relative "bibxml/to_rfcxml_referencegroup"
|
|
3
|
+
require_relative "bibxml/to_rfcxml_v3"
|
|
4
|
+
require_relative "bibxml/to_rfcxml_referencegroup_v3"
|
|
3
5
|
require_relative "bibxml/from_rfcxml"
|
|
4
6
|
require_relative "bibxml/from_rfcxml_referencegroup"
|
|
5
7
|
|
|
@@ -16,17 +18,27 @@ module Relaton
|
|
|
16
18
|
RFCPREFIXES = %w[RFC BCP FYI STD].freeze
|
|
17
19
|
|
|
18
20
|
# Forward: ItemData -> Rfcxml model
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
#
|
|
22
|
+
# @param v3 [Boolean] emit strict RFC 7991 (xml2rfc v3) instead of the
|
|
23
|
+
# BibXML shape the relaton data fetchers publish
|
|
24
|
+
# @param anchor [String, nil] anchor to use instead of the one derived
|
|
25
|
+
# from the item's docnumber/docidentifier
|
|
26
|
+
def self.from_item(item, include_keywords: true, v3: false, anchor: nil) # rubocop:disable Naming/MethodParameterName
|
|
27
|
+
klass = if v3 then v3_converter(item)
|
|
28
|
+
elsif bcp?(item) then ToRfcxmlReferencegroup
|
|
29
|
+
else ToRfcxml
|
|
30
|
+
end
|
|
31
|
+
klass.new(item, include_keywords: include_keywords,
|
|
32
|
+
anchor: anchor).transform
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.v3_converter(item)
|
|
36
|
+
if item.relation.any? { |rel| rel.type == "includes" }
|
|
37
|
+
ToRfcxmlReferencegroupV3
|
|
38
|
+
else ToRfcxmlV3
|
|
28
39
|
end
|
|
29
40
|
end
|
|
41
|
+
private_class_method :v3_converter
|
|
30
42
|
|
|
31
43
|
def self.bcp?(item) # rubocop:disable Metrics/CyclomaticComplexity
|
|
32
44
|
item.docnumber&.match(/^BCP/) ||
|
|
@@ -19,8 +19,18 @@ module Relaton
|
|
|
19
19
|
COLLECTION_WRITE_ONLY_ATTRIBUTES = %i[title abstract source relation].freeze
|
|
20
20
|
|
|
21
21
|
ATTRIBUTES.each { |attr| attr_accessor attr }
|
|
22
|
-
COLLECTION_ATTRBUTES.each { |attr|
|
|
23
|
-
|
|
22
|
+
COLLECTION_ATTRBUTES.each { |attr| attr_reader attr }
|
|
23
|
+
|
|
24
|
+
# Collections are always arrays. lutaml-model assigns every mapped
|
|
25
|
+
# attribute after building the instance, so an element absent from the
|
|
26
|
+
# XML would otherwise overwrite the [] set in #initialize with nil --
|
|
27
|
+
# `Item.from_xml` items then blew up in any consumer that iterated them,
|
|
28
|
+
# while `from_yaml` ones did not.
|
|
29
|
+
(COLLECTION_ATTRBUTES + COLLECTION_WRITE_ONLY_ATTRIBUTES).each do |attr|
|
|
30
|
+
define_method("#{attr}=") do |value|
|
|
31
|
+
instance_variable_set("@#{attr}", array(value))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
24
34
|
|
|
25
35
|
attr_reader :id, :docidentifier
|
|
26
36
|
|
|
@@ -28,10 +38,10 @@ module Relaton
|
|
|
28
38
|
ATTRIBUTES.each { |attr| instance_variable_set("@#{attr}", args[attr]) }
|
|
29
39
|
|
|
30
40
|
(COLLECTION_ATTRBUTES + COLLECTION_WRITE_ONLY_ATTRIBUTES).each do |attr|
|
|
31
|
-
instance_variable_set("@#{attr}", args[attr]
|
|
41
|
+
instance_variable_set("@#{attr}", array(args[attr]))
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
@docidentifier = args[:docidentifier]
|
|
44
|
+
@docidentifier = array(args[:docidentifier])
|
|
35
45
|
self.schema_version = schema
|
|
36
46
|
@id = args[:id]
|
|
37
47
|
create_id
|
|
@@ -42,7 +52,7 @@ module Relaton
|
|
|
42
52
|
end
|
|
43
53
|
|
|
44
54
|
def docidentifier=(value)
|
|
45
|
-
@docidentifier = value
|
|
55
|
+
@docidentifier = array(value)
|
|
46
56
|
create_id
|
|
47
57
|
end
|
|
48
58
|
|
|
@@ -156,8 +166,10 @@ module Relaton
|
|
|
156
166
|
Converter::Asciibib.from_item(self)
|
|
157
167
|
end
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
# @param opts [Hash] passed to {Converter::BibXml.from_item}; notably
|
|
170
|
+
# `v3: true` for strict RFC 7991 output instead of BibXML.
|
|
171
|
+
def to_rfcxml(**)
|
|
172
|
+
Converter::BibXml.from_item(self, **).to_xml
|
|
161
173
|
end
|
|
162
174
|
|
|
163
175
|
def deep_clone
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module Relaton
|
|
2
2
|
module Bib
|
|
3
3
|
class Series < Lutaml::Model::Serializable
|
|
4
|
-
|
|
4
|
+
# "stream" carries an RFC publication stream; relaton-ietf already builds
|
|
5
|
+
# such series, and biblio.rng's SeriesType admits any string.
|
|
6
|
+
attribute :type, :string, values: %w[main alt stream]
|
|
5
7
|
attribute :formattedref, Formattedref
|
|
6
8
|
attribute :title, Title, collection: (1..)
|
|
7
9
|
attribute :place, Place
|
data/lib/relaton/bib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: relaton-bib
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.8
|
|
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
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bibtex-ruby
|
|
@@ -171,6 +171,8 @@ files:
|
|
|
171
171
|
- lib/relaton/bib/converter/bibxml/from_rfcxml_referencegroup.rb
|
|
172
172
|
- lib/relaton/bib/converter/bibxml/to_rfcxml.rb
|
|
173
173
|
- lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb
|
|
174
|
+
- lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb
|
|
175
|
+
- lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb
|
|
174
176
|
- lib/relaton/bib/hash_parser_v1.rb
|
|
175
177
|
- lib/relaton/bib/item_data.rb
|
|
176
178
|
- lib/relaton/bib/model/abstract.rb
|