relaton-bib 2.1.8 → 2.1.9
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 +3 -15
- data/README.adoc +0 -64
- data/lib/relaton/bib/converter/bibxml/from_rfcxml.rb +1 -12
- data/lib/relaton/bib/converter/bibxml/to_rfcxml.rb +8 -32
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb +3 -10
- data/lib/relaton/bib/converter/bibxml.rb +9 -21
- data/lib/relaton/bib/item_data.rb +7 -19
- data/lib/relaton/bib/model/series.rb +1 -3
- data/lib/relaton/bib/sanitizer.rb +139 -3
- data/lib/relaton/bib/version.rb +1 -1
- metadata +1 -3
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb +0 -43
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb +0 -416
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74bf6a3c3abe13cbadc903c2d2b811b125f654033e092ed33305834663404854
|
|
4
|
+
data.tar.gz: 396420681c9fe3567117b7e6a992c56a0cbfd3b219d36d650d8deb072a36435b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fdb28c540d26dd47ac56af80c292cae33f6cafa96672161d732f85e457b4a7336c2c5f11d5f5b852284433f73b08cfefe7d2af0c1e2d99e1557c05f985f46da
|
|
7
|
+
data.tar.gz: 1585a8df0f2a2ad420eb26ed1d11a6f36483f90f10510f3cb63f72145d0d401b4499016100475b37195f31033d4c600bd281d8d4f97b7f648732212f7a5378b2
|
data/CLAUDE.md
CHANGED
|
@@ -77,17 +77,7 @@ 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
|
|
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.
|
|
80
|
+
- **`Converter::BibXml`** - Converts `ItemData` to RFC XML format via `Relaton::Bib::Converter::BibXml.to_xml`
|
|
91
81
|
- **lutaml-model native** - `to_xml`, `to_yaml`, `to_json` via the serialization classes
|
|
92
82
|
|
|
93
83
|
### Usage Pattern
|
|
@@ -108,13 +98,11 @@ item.to_xml # as <bibitem>
|
|
|
108
98
|
item.to_xml(bibdata: true) # as <bibdata>
|
|
109
99
|
item.to_yaml
|
|
110
100
|
item.to_bibtex
|
|
111
|
-
item.to_rfcxml
|
|
112
|
-
item.to_rfcxml(v3: true) # strict RFC 7991 / xml2rfc v3
|
|
113
|
-
item.to_rfcxml(anchor: "ISO712", include_keywords: false)
|
|
101
|
+
item.to_rfcxml
|
|
114
102
|
```
|
|
115
103
|
|
|
116
104
|
## Code Style
|
|
117
105
|
|
|
118
106
|
- Follows Ribose OSS Ruby style guide (inherited via `.rubocop.yml`)
|
|
119
|
-
- Target Ruby version: 3.
|
|
107
|
+
- Target Ruby version: 3.1+
|
|
120
108
|
- Uses YARD documentation comments
|
data/README.adoc
CHANGED
|
@@ -334,70 +334,6 @@ 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
|
-
|
|
401
337
|
=== Logging
|
|
402
338
|
|
|
403
339
|
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,18 +326,7 @@ 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
|
|
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")]
|
|
329
|
+
end.compact
|
|
341
330
|
end
|
|
342
331
|
|
|
343
332
|
# --- Keyword ---
|
|
@@ -5,15 +5,14 @@ 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)
|
|
9
9
|
@item = item
|
|
10
10
|
@include_keywords = include_keywords
|
|
11
|
-
@anchor = anchor
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def transform
|
|
15
14
|
model = ::Rfcxml::V3::Reference.new
|
|
16
|
-
model.anchor = @
|
|
15
|
+
model.anchor = @item.docnumber || derive_anchor
|
|
17
16
|
model.target = create_target
|
|
18
17
|
model.front = create_front
|
|
19
18
|
model.format = create_format
|
|
@@ -27,22 +26,14 @@ module Relaton
|
|
|
27
26
|
di&.content&.to_s&.gsub(" ", ".")
|
|
28
27
|
end
|
|
29
28
|
|
|
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
|
-
|
|
35
29
|
def create_target
|
|
36
|
-
target =
|
|
30
|
+
target = @item.source.detect { |l| l.type.casecmp("src").zero? } ||
|
|
31
|
+
@item.source.detect { |l| l.type.casecmp("doi").zero? }
|
|
37
32
|
return unless target
|
|
38
33
|
|
|
39
34
|
target.content.to_s
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
def source_of_type(type)
|
|
43
|
-
@item.source.detect { |l| l.type&.casecmp(type)&.zero? }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
37
|
def create_front # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
47
38
|
front = ::Rfcxml::V3::Front.new
|
|
48
39
|
front.title = Rfcxml::V3::Title.new(content: @item.title[0].content) if @item.title.any?
|
|
@@ -68,16 +59,9 @@ module Relaton
|
|
|
68
59
|
end
|
|
69
60
|
end
|
|
70
61
|
|
|
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
|
-
|
|
78
62
|
def series_to_seriesinfo # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
79
63
|
@item.series.select do |s|
|
|
80
|
-
|
|
64
|
+
s.title.reject { |t| t.content == "DOI" }.any?
|
|
81
65
|
end.uniq do |s|
|
|
82
66
|
s.title.find { |t| t.content != "DOI" }.content
|
|
83
67
|
end.each_with_object([]) do |s, si|
|
|
@@ -87,13 +71,8 @@ module Relaton
|
|
|
87
71
|
end
|
|
88
72
|
end
|
|
89
73
|
|
|
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
|
-
|
|
95
74
|
def create_authors # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
96
|
-
|
|
75
|
+
@item.contributor.reject { |c| committee_contributor?(c) }.map do |contrib|
|
|
97
76
|
role = "editor" if contrib.role.detect { |r| r.type == "editor" }
|
|
98
77
|
Rfcxml::V3::Author.new(
|
|
99
78
|
role: role,
|
|
@@ -305,10 +284,7 @@ module Relaton
|
|
|
305
284
|
return unless @item.abstract.any?
|
|
306
285
|
|
|
307
286
|
content = @item.abstract[0].content
|
|
308
|
-
|
|
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
|
|
287
|
+
paragraphs = content.scan(%r{<p>(.*?)</p>}m).flatten
|
|
312
288
|
paragraphs = [content] if paragraphs.empty?
|
|
313
289
|
ts = paragraphs.map { |p| Rfcxml::V3::Text.new(content: CGI.unescapeHTML(p)) }
|
|
314
290
|
Rfcxml::V3::Abstract.new(t: ts)
|
|
@@ -318,7 +294,7 @@ module Relaton
|
|
|
318
294
|
|
|
319
295
|
def create_format # rubocop:disable Metrics/AbcSize
|
|
320
296
|
@item.source.each_with_object([]) do |l, a|
|
|
321
|
-
next unless FORMAT_TYPES.any? { |ft| l.type
|
|
297
|
+
next unless FORMAT_TYPES.any? { |ft| l.type.casecmp(ft).zero? }
|
|
322
298
|
|
|
323
299
|
a << Rfcxml::V3::Format.new(
|
|
324
300
|
type: l.type, target: l.content,
|
|
@@ -3,10 +3,9 @@ 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)
|
|
7
7
|
@item = item
|
|
8
8
|
@include_keywords = include_keywords
|
|
9
|
-
@anchor = anchor
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def transform
|
|
@@ -31,8 +30,6 @@ module Relaton
|
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def create_anchor
|
|
34
|
-
return @anchor if @anchor
|
|
35
|
-
|
|
36
33
|
docid = @item.docidentifier.detect(&:primary) ||
|
|
37
34
|
@item.docidentifier[0]
|
|
38
35
|
return unless docid
|
|
@@ -41,13 +38,9 @@ module Relaton
|
|
|
41
38
|
id.sub(/^(RFC|BCP|FYI|STD) /, '\1').sub(/^\w+\./, "")
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
# An untyped <uri> is legal, so guard before comparing.
|
|
45
|
-
def target_types = %w[src doi]
|
|
46
|
-
|
|
47
41
|
def create_target
|
|
48
|
-
target =
|
|
49
|
-
@item.source.detect { |l| l.type
|
|
50
|
-
end.first
|
|
42
|
+
target = @item.source.detect { |l| l.type.casecmp("src").zero? } ||
|
|
43
|
+
@item.source.detect { |l| l.type.casecmp("doi").zero? }
|
|
51
44
|
return unless target
|
|
52
45
|
|
|
53
46
|
target.content.to_s
|
|
@@ -1,7 +1,5 @@
|
|
|
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"
|
|
5
3
|
require_relative "bibxml/from_rfcxml"
|
|
6
4
|
require_relative "bibxml/from_rfcxml_referencegroup"
|
|
7
5
|
|
|
@@ -18,27 +16,17 @@ module Relaton
|
|
|
18
16
|
RFCPREFIXES = %w[RFC BCP FYI STD].freeze
|
|
19
17
|
|
|
20
18
|
# Forward: ItemData -> Rfcxml model
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
19
|
+
def self.from_item(item, include_keywords: true) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
20
|
+
if bcp?(item)
|
|
21
|
+
ToRfcxmlReferencegroup.new(
|
|
22
|
+
item, include_keywords: include_keywords
|
|
23
|
+
).transform
|
|
24
|
+
else
|
|
25
|
+
ToRfcxml.new(
|
|
26
|
+
item, include_keywords: include_keywords
|
|
27
|
+
).transform
|
|
39
28
|
end
|
|
40
29
|
end
|
|
41
|
-
private_class_method :v3_converter
|
|
42
30
|
|
|
43
31
|
def self.bcp?(item) # rubocop:disable Metrics/CyclomaticComplexity
|
|
44
32
|
item.docnumber&.match(/^BCP/) ||
|
|
@@ -19,18 +19,8 @@ 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
|
-
|
|
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
|
|
22
|
+
COLLECTION_ATTRBUTES.each { |attr| attr_accessor attr }
|
|
23
|
+
COLLECTION_WRITE_ONLY_ATTRIBUTES.each { |attr| attr_writer attr }
|
|
34
24
|
|
|
35
25
|
attr_reader :id, :docidentifier
|
|
36
26
|
|
|
@@ -38,10 +28,10 @@ module Relaton
|
|
|
38
28
|
ATTRIBUTES.each { |attr| instance_variable_set("@#{attr}", args[attr]) }
|
|
39
29
|
|
|
40
30
|
(COLLECTION_ATTRBUTES + COLLECTION_WRITE_ONLY_ATTRIBUTES).each do |attr|
|
|
41
|
-
instance_variable_set("@#{attr}",
|
|
31
|
+
instance_variable_set("@#{attr}", args[attr] || [])
|
|
42
32
|
end
|
|
43
33
|
|
|
44
|
-
@docidentifier =
|
|
34
|
+
@docidentifier = args[:docidentifier] || []
|
|
45
35
|
self.schema_version = schema
|
|
46
36
|
@id = args[:id]
|
|
47
37
|
create_id
|
|
@@ -52,7 +42,7 @@ module Relaton
|
|
|
52
42
|
end
|
|
53
43
|
|
|
54
44
|
def docidentifier=(value)
|
|
55
|
-
@docidentifier =
|
|
45
|
+
@docidentifier = value || []
|
|
56
46
|
create_id
|
|
57
47
|
end
|
|
58
48
|
|
|
@@ -166,10 +156,8 @@ module Relaton
|
|
|
166
156
|
Converter::Asciibib.from_item(self)
|
|
167
157
|
end
|
|
168
158
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def to_rfcxml(**)
|
|
172
|
-
Converter::BibXml.from_item(self, **).to_xml
|
|
159
|
+
def to_rfcxml
|
|
160
|
+
Converter::BibXml.from_item(self).to_xml
|
|
173
161
|
end
|
|
174
162
|
|
|
175
163
|
def deep_clone
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
module Relaton
|
|
2
2
|
module Bib
|
|
3
3
|
class Series < Lutaml::Model::Serializable
|
|
4
|
-
|
|
5
|
-
# such series, and biblio.rng's SeriesType admits any string.
|
|
6
|
-
attribute :type, :string, values: %w[main alt stream]
|
|
4
|
+
attribute :type, :string, values: %w[main alt]
|
|
7
5
|
attribute :formattedref, Formattedref
|
|
8
6
|
attribute :title, Title, collection: (1..)
|
|
9
7
|
attribute :place, Place
|
|
@@ -42,15 +42,151 @@ module Relaton
|
|
|
42
42
|
|
|
43
43
|
TAG_RX = %r{<[a-zA-Z/!?]}
|
|
44
44
|
|
|
45
|
+
# Captures a namespace prefix, on a tag or on an attribute: the
|
|
46
|
+
# "jats" of <jats:p> and </jats:italic>, and the "xlink" of
|
|
47
|
+
# xlink:href.
|
|
48
|
+
NS_PREFIX_RX = %r{(?:</?|\s)([A-Za-z_][\w.-]*):(?=[A-Za-z_])}
|
|
49
|
+
|
|
50
|
+
# Namespace that declares a prefix which the content leaves
|
|
51
|
+
# undeclared. The sanitiser removes it again before it serialises.
|
|
52
|
+
NS_PLACEHOLDER = "urn:x-relaton-undeclared:%s".freeze
|
|
53
|
+
|
|
54
|
+
# Element that carries the placeholder declarations. Its children
|
|
55
|
+
# are the sanitised content, so the element itself never reaches
|
|
56
|
+
# the output.
|
|
57
|
+
NS_WRAPPER = "relaton-sanitizer-root".freeze
|
|
58
|
+
|
|
59
|
+
# Reserved prefixes. XML declares both, so the content must not.
|
|
60
|
+
NS_RESERVED = %w[xml xmlns].freeze
|
|
61
|
+
|
|
62
|
+
# Serialise without the FORMAT option, so the sanitiser keeps the
|
|
63
|
+
# shape of element-only content instead of adding newlines and
|
|
64
|
+
# indent.
|
|
65
|
+
SAVE_OPTS = Nokogiri::XML::Node::SaveOptions::AS_XML
|
|
66
|
+
|
|
45
67
|
def self.sanitize(content)
|
|
46
68
|
return content unless sanitizable?(content)
|
|
47
69
|
|
|
70
|
+
node = parse(content)
|
|
71
|
+
return content if node.nil?
|
|
72
|
+
|
|
73
|
+
sanitize_children(node)
|
|
74
|
+
node.children.map do |c|
|
|
75
|
+
c.to_xml(encoding: "UTF-8", save_with: SAVE_OPTS)
|
|
76
|
+
end.join
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# Parse the content into a node whose children are the content.
|
|
81
|
+
#
|
|
82
|
+
# @param [String] content The raw marked-up content.
|
|
83
|
+
#
|
|
84
|
+
# @return [Nokogiri::XML::Node, nil] The node, or nil when the
|
|
85
|
+
# content does not parse.
|
|
86
|
+
#
|
|
87
|
+
def self.parse(content)
|
|
48
88
|
fragment = Nokogiri::XML::DocumentFragment.parse(content)
|
|
49
|
-
return
|
|
89
|
+
return fragment if fragment.errors.empty?
|
|
90
|
+
|
|
91
|
+
parse_with_prefixes(content)
|
|
92
|
+
end
|
|
93
|
+
private_class_method :parse
|
|
50
94
|
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
#
|
|
96
|
+
# Parse content that uses undeclared namespace prefixes.
|
|
97
|
+
#
|
|
98
|
+
# An undeclared prefix is always a parse error, so without this the
|
|
99
|
+
# sanitiser gives up on exactly the third-party markup that needs
|
|
100
|
+
# sanitising most. Declare every prefix that the content uses on a
|
|
101
|
+
# wrapper element, parse, then remove the placeholder namespaces
|
|
102
|
+
# from the elements and from the attributes. See metanorma-pdfa#99.
|
|
103
|
+
#
|
|
104
|
+
# An undeclared prefix inside an OPAQUE <stem> goes as well. The
|
|
105
|
+
# sanitiser cannot keep it: an undeclared prefix in the output is
|
|
106
|
+
# the exact failure that this method removes. Only a namespace that
|
|
107
|
+
# the content declares itself survives verbatim.
|
|
108
|
+
#
|
|
109
|
+
# @param [String] content The raw marked-up content.
|
|
110
|
+
#
|
|
111
|
+
# @return [Nokogiri::XML::Element, nil] The wrapper element, or nil
|
|
112
|
+
# when the content uses no prefix or does not parse.
|
|
113
|
+
#
|
|
114
|
+
def self.parse_with_prefixes(content)
|
|
115
|
+
decl = placeholder_declarations(content) or return
|
|
116
|
+
name = wrapper_name(content)
|
|
117
|
+
doc = Nokogiri::XML "<#{name} #{decl}>#{content}</#{name}>"
|
|
118
|
+
return unless doc.errors.empty?
|
|
119
|
+
|
|
120
|
+
drop_placeholder_namespaces doc.root
|
|
121
|
+
end
|
|
122
|
+
private_class_method :parse_with_prefixes
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# Declare every namespace prefix that the content uses.
|
|
126
|
+
#
|
|
127
|
+
# @param [String] content The raw marked-up content.
|
|
128
|
+
#
|
|
129
|
+
# @return [String, nil] The declarations, or nil when the content
|
|
130
|
+
# uses no prefix.
|
|
131
|
+
#
|
|
132
|
+
def self.placeholder_declarations(content)
|
|
133
|
+
prefixes = content.scan(NS_PREFIX_RX).flatten.uniq - NS_RESERVED
|
|
134
|
+
return if prefixes.empty?
|
|
135
|
+
|
|
136
|
+
prefixes.map do |pfx|
|
|
137
|
+
%(xmlns:#{pfx}="#{format NS_PLACEHOLDER, pfx}")
|
|
138
|
+
end.join(" ")
|
|
139
|
+
end
|
|
140
|
+
private_class_method :placeholder_declarations
|
|
141
|
+
|
|
142
|
+
#
|
|
143
|
+
# Name a wrapper element that the content does not close itself.
|
|
144
|
+
#
|
|
145
|
+
# Content that holds the literal end tag of the wrapper would close
|
|
146
|
+
# it early. The document then has more than one root, the parse
|
|
147
|
+
# fails, and the sanitiser gives up on content that it can handle.
|
|
148
|
+
#
|
|
149
|
+
# @param [String] content The raw marked-up content.
|
|
150
|
+
#
|
|
151
|
+
# @return [String] A name that the content does not contain.
|
|
152
|
+
#
|
|
153
|
+
def self.wrapper_name(content)
|
|
154
|
+
name = NS_WRAPPER
|
|
155
|
+
name += "-x" while content.include?(name)
|
|
156
|
+
name
|
|
157
|
+
end
|
|
158
|
+
private_class_method :wrapper_name
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
# Remove the placeholder namespaces, and only those.
|
|
162
|
+
#
|
|
163
|
+
# Nokogiri's remove_namespaces! would also strip a namespace that
|
|
164
|
+
# the content declares itself, such as the MathML xmlns inside an
|
|
165
|
+
# OPAQUE <stem>, which must survive verbatim. Match the wrapper's
|
|
166
|
+
# own declarations, so a namespace of the content never matches,
|
|
167
|
+
# whatever its URI.
|
|
168
|
+
#
|
|
169
|
+
# The declarations stay on the wrapper element. Only its children
|
|
170
|
+
# reach the output, so the declarations never leak. Do not
|
|
171
|
+
# serialise the root itself.
|
|
172
|
+
#
|
|
173
|
+
# @param [Nokogiri::XML::Element] root The wrapper element.
|
|
174
|
+
#
|
|
175
|
+
# @return [Nokogiri::XML::Element] The same element.
|
|
176
|
+
#
|
|
177
|
+
def self.drop_placeholder_namespaces(root)
|
|
178
|
+
placeholders = root.namespace_definitions
|
|
179
|
+
root.traverse do |node|
|
|
180
|
+
node.namespace = nil if placeholders.include?(node.namespace)
|
|
181
|
+
next unless node.element?
|
|
182
|
+
|
|
183
|
+
node.attribute_nodes.each do |attr|
|
|
184
|
+
attr.namespace = nil if placeholders.include?(attr.namespace)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
root
|
|
53
188
|
end
|
|
189
|
+
private_class_method :drop_placeholder_namespaces
|
|
54
190
|
|
|
55
191
|
def self.sanitizable?(content)
|
|
56
192
|
content.is_a?(::String) && !content.empty? && content.match?(TAG_RX)
|
data/lib/relaton/bib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -171,8 +171,6 @@ 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
|
|
176
174
|
- lib/relaton/bib/hash_parser_v1.rb
|
|
177
175
|
- lib/relaton/bib/item_data.rb
|
|
178
176
|
- lib/relaton/bib/model/abstract.rb
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
|
@@ -1,416 +0,0 @@
|
|
|
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
|