relaton 3.0.6 → 3.0.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/bin/relaton +20 -0
- data/lib/relaton/bib/model/docidentifier.rb +17 -2
- data/lib/relaton/bibcollection.rb +146 -0
- data/lib/relaton/bibdata.rb +92 -0
- data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
- data/lib/relaton/cli/base_convertor.rb +134 -0
- data/lib/relaton/cli/command.rb +390 -0
- data/lib/relaton/cli/data_fetcher.rb +20 -0
- data/lib/relaton/cli/frontend/dist/404.html +18 -0
- data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
- data/lib/relaton/cli/frontend/dist/style.css +3 -0
- data/lib/relaton/cli/frontend/index.html +14 -0
- data/lib/relaton/cli/frontend/package-lock.json +2945 -0
- data/lib/relaton/cli/frontend/package.json +29 -0
- data/lib/relaton/cli/frontend/public/404.html +18 -0
- data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
- data/lib/relaton/cli/frontend/src/App.vue +506 -0
- data/lib/relaton/cli/frontend/src/app.ts +90 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
- data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
- data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
- data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
- data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
- data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
- data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
- data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
- data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
- data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
- data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
- data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
- data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
- data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
- data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
- data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
- data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
- data/lib/relaton/cli/frontend/tsconfig.json +20 -0
- data/lib/relaton/cli/frontend/vite.config.ts +42 -0
- data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
- data/lib/relaton/cli/frontend_assets.rb +58 -0
- data/lib/relaton/cli/full_text_search.rb +98 -0
- data/lib/relaton/cli/i18n.yaml +1 -0
- data/lib/relaton/cli/index_item_normalizer.rb +380 -0
- data/lib/relaton/cli/index_site_generator.rb +1015 -0
- data/lib/relaton/cli/relaton_file.rb +290 -0
- data/lib/relaton/cli/subcommand_collection.rb +263 -0
- data/lib/relaton/cli/subcommand_db.rb +81 -0
- data/lib/relaton/cli/templates/_document.liquid +111 -0
- data/lib/relaton/cli/templates/_index.liquid +61 -0
- data/lib/relaton/cli/templates/index/page.liquid +20 -0
- data/lib/relaton/cli/templates/index-style.css +1017 -0
- data/lib/relaton/cli/util.rb +8 -0
- data/lib/relaton/cli/version.rb +11 -0
- data/lib/relaton/cli/xml_convertor.rb +47 -0
- data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
- data/lib/relaton/cli/yaml_convertor.rb +102 -0
- data/lib/relaton/cli.rb +113 -0
- data/lib/relaton/doi/CLAUDE.md +1 -1
- data/lib/relaton/element_finder.rb +29 -0
- data/lib/relaton/gb/CLAUDE.md +1 -1
- data/lib/relaton/ietf/CLAUDE.md +1 -1
- data/lib/relaton/iho/CLAUDE.md +3 -3
- data/lib/relaton/iho/bibliography.rb +41 -27
- data/lib/relaton/iho/processor.rb +3 -2
- data/lib/relaton/nist/CLAUDE.md +11 -1
- data/lib/relaton/nist/README.adoc +7 -1
- data/lib/relaton/nist/hit_collection.rb +25 -1
- data/lib/relaton/nist/mods_parser.rb +32 -8
- data/lib/relaton/nist/pubs_export.rb +69 -11
- data/lib/relaton/ogc/CLAUDE.md +1 -1
- data/lib/relaton/omg/CLAUDE.md +1 -1
- data/lib/relaton/plateau/CLAUDE.md +1 -1
- data/lib/relaton/render/general.rb +319 -0
- data/lib/relaton/render/iso690/csl_import.rb +129 -0
- data/lib/relaton/render/iso690/element.rb +153 -0
- data/lib/relaton/render/iso690/elements/access.rb +44 -0
- data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
- data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
- data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
- data/lib/relaton/render/iso690/elements/creator.rb +267 -0
- data/lib/relaton/render/iso690/elements/edition.rb +97 -0
- data/lib/relaton/render/iso690/elements/extent.rb +71 -0
- data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
- data/lib/relaton/render/iso690/elements/location.rb +73 -0
- data/lib/relaton/render/iso690/elements/medium.rb +32 -0
- data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
- data/lib/relaton/render/iso690/elements/production.rb +111 -0
- data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
- data/lib/relaton/render/iso690/elements/series.rb +66 -0
- data/lib/relaton/render/iso690/elements/size.rb +45 -0
- data/lib/relaton/render/iso690/elements/status.rb +30 -0
- data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
- data/lib/relaton/render/iso690/elements/title.rb +56 -0
- data/lib/relaton/render/iso690/elements/updated.rb +38 -0
- data/lib/relaton/render/iso690/elements.rb +67 -0
- data/lib/relaton/render/iso690/fields.rb +142 -0
- data/lib/relaton/render/iso690/i18n/en.yml +48 -0
- data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
- data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
- data/lib/relaton/render/iso690/i18n.rb +83 -0
- data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
- data/lib/relaton/render/iso690/index_renderer.rb +57 -0
- data/lib/relaton/render/iso690/kinds.rb +53 -0
- data/lib/relaton/render/iso690/renderer.rb +150 -0
- data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
- data/lib/relaton/render/iso690/rules/jis.rb +103 -0
- data/lib/relaton/render/iso690/rules/nist.rb +384 -0
- data/lib/relaton/render/iso690/rules.rb +227 -0
- data/lib/relaton/render/iso690/style/locale.rb +59 -0
- data/lib/relaton/render/iso690/style/name_form.rb +87 -0
- data/lib/relaton/render/iso690/style/scheme.rb +33 -0
- data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
- data/lib/relaton/render/iso690/style/template_map.rb +40 -0
- data/lib/relaton/render/iso690/style/type_template.rb +45 -0
- data/lib/relaton/render/iso690/style.rb +227 -0
- data/lib/relaton/render/iso690/styles/apa.yml +45 -0
- data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
- data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
- data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
- data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
- data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
- data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
- data/lib/relaton/render/iso690/styles/mla.yml +56 -0
- data/lib/relaton/render/iso690/template.rb +248 -0
- data/lib/relaton/render/iso690.rb +28 -0
- data/lib/relaton/render/version.rb +5 -0
- data/lib/relaton/un/CLAUDE.md +1 -1
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton-cli.rb +17 -0
- data/lib/relaton-render.rb +13 -0
- metadata +192 -21
|
@@ -12,6 +12,12 @@ module Relaton
|
|
|
12
12
|
|
|
13
13
|
GHNISTDATA = "https://raw.githubusercontent.com/relaton/relaton-data-nist/v2/"
|
|
14
14
|
|
|
15
|
+
# Errors of the local CSRC cache (read-only or full disk, a bad zip).
|
|
16
|
+
LOCAL_DATA_ERRORS = [
|
|
17
|
+
Errno::EROFS, Errno::EACCES, Errno::EPERM, Errno::ENOSPC,
|
|
18
|
+
Errno::ENOENT, Zip::Error
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
15
21
|
attr_reader :reference
|
|
16
22
|
attr_accessor :array
|
|
17
23
|
|
|
@@ -47,7 +53,7 @@ module Relaton
|
|
|
47
53
|
# @return [Relaton::Nist::HitCollection] hits collection
|
|
48
54
|
#
|
|
49
55
|
def search
|
|
50
|
-
@array =
|
|
56
|
+
@array = from_json_or_empty
|
|
51
57
|
@array = from_ga unless @array.any?
|
|
52
58
|
sort_hits!
|
|
53
59
|
end
|
|
@@ -400,6 +406,24 @@ module Relaton
|
|
|
400
406
|
select_data.map { |h| Hit.new(hit_data(h), self) }
|
|
401
407
|
end
|
|
402
408
|
|
|
409
|
+
#
|
|
410
|
+
# The CSRC hits, or none when the local CSRC data cannot be used: a
|
|
411
|
+
# file-system error must not stop the search, because the
|
|
412
|
+
# relaton-data-nist index can still answer (relaton-nist#57). Network
|
|
413
|
+
# errors are not rescued: they still reach Bibliography.search, which
|
|
414
|
+
# raises Relaton::RequestError, so a CSRC outage is not cached as
|
|
415
|
+
# "not found".
|
|
416
|
+
#
|
|
417
|
+
# @return [Array<Relaton::Nist::Hit>] hits
|
|
418
|
+
#
|
|
419
|
+
def from_json_or_empty
|
|
420
|
+
from_json
|
|
421
|
+
rescue *LOCAL_DATA_ERRORS => e
|
|
422
|
+
Util.warn "CSRC pubs-export data is not available (#{e.message}); " \
|
|
423
|
+
"searching the Relaton repository only", key: @reference
|
|
424
|
+
[]
|
|
425
|
+
end
|
|
426
|
+
|
|
403
427
|
def pubs_export_id(json)
|
|
404
428
|
self.class.pubs_export_id json
|
|
405
429
|
end
|
|
@@ -13,7 +13,7 @@ module Relaton
|
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
15
|
ATTRS = %i[type docidentifier title source abstract date contributor
|
|
16
|
-
relation place series].freeze
|
|
16
|
+
relation place series edition].freeze
|
|
17
17
|
|
|
18
18
|
def initialize(doc, series, errors = {})
|
|
19
19
|
@doc = doc
|
|
@@ -51,17 +51,41 @@ module Relaton
|
|
|
51
51
|
def get_id_from_str(str)
|
|
52
52
|
return if str.nil? || str.empty?
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
# parses through a path that skips UpdateCodes date lookup and drops
|
|
56
|
-
# letter suffixes. Space the leading "PUB.SERIES." prefix so it parses
|
|
57
|
-
# like the canonical short form (only the separator dots are touched,
|
|
58
|
-
# not any inside the number). Then force :human rendering.
|
|
59
|
-
spaced = str.sub(/\A(NIST|NBS)\.([A-Z][A-Za-z]*)\./, '\1 \2 ')
|
|
60
|
-
::Pubid::Nist::Identifier.parse(spaced).to_s(format: :human)
|
|
54
|
+
parse_pubid(str).to_s(format: :human)
|
|
61
55
|
rescue ::Pubid::Errors::ParseError
|
|
62
56
|
str.gsub(".", " ").sub(/^[\D]+/, &:upcase)
|
|
63
57
|
end
|
|
64
58
|
|
|
59
|
+
# DOIs arrive dotted ("NIST.HB.135e2022-upd1"); that MR-style form
|
|
60
|
+
# parses through a path that skips UpdateCodes date lookup and drops
|
|
61
|
+
# letter suffixes. Space the leading "PUB.SERIES." prefix so it parses
|
|
62
|
+
# like the canonical short form (only the separator dots are touched,
|
|
63
|
+
# not any inside the number).
|
|
64
|
+
def parse_pubid(str)
|
|
65
|
+
spaced = str.sub(/\A(NIST|NBS)\.([A-Z][A-Za-z]*)\./, '\1 \2 ')
|
|
66
|
+
::Pubid::Nist::Identifier.parse(spaced)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The edition number a DOI states ("NBS.CIRC.111e2" -> 2), relaton-nist#56.
|
|
70
|
+
# The MODS carries no edition number of its own (its <edition> is prose:
|
|
71
|
+
# "New version", "Erratum"), so the DOI is the only source. Only a
|
|
72
|
+
# numbered "e<N>" edition counts: an "e<year>" edition is a date, and an
|
|
73
|
+
# "r<N>" revision is not an edition. An edition 0 counts as 1 (the issue's
|
|
74
|
+
# second rule; no live DOI has one).
|
|
75
|
+
# @return [Bib::Edition, nil]
|
|
76
|
+
def parse_edition
|
|
77
|
+
doi = parse_doi
|
|
78
|
+
return if doi.nil? || doi.empty?
|
|
79
|
+
|
|
80
|
+
edition = parse_pubid(doi).edition
|
|
81
|
+
return unless edition&.type == "e" && edition.id.to_s.match?(/\A\d{1,3}\z/)
|
|
82
|
+
|
|
83
|
+
number = edition.id.to_i.zero? ? "1" : edition.id
|
|
84
|
+
Bib::Edition.new(number: number, content: number)
|
|
85
|
+
rescue ::Pubid::Errors::Error, ArgumentError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
65
89
|
# @return [String]
|
|
66
90
|
def replace_wrong_doi(id)
|
|
67
91
|
case id
|
|
@@ -8,6 +8,8 @@ module Relaton
|
|
|
8
8
|
|
|
9
9
|
DOMAIN = "https://csrc.nist.gov".freeze
|
|
10
10
|
PUBS_EXPORT = URI.join(DOMAIN, "/CSRC/media/feeds/metanorma/pubs-export")
|
|
11
|
+
# The default cache location. The live location is #datafile, which
|
|
12
|
+
# follows Relaton::Index.config.storage_dir (default Dir.home).
|
|
11
13
|
DATAFILEDIR = File.expand_path ".relaton/nist", Dir.home
|
|
12
14
|
DATAFILE = File.expand_path "pubs-export.zip", DATAFILEDIR
|
|
13
15
|
|
|
@@ -23,8 +25,8 @@ module Relaton
|
|
|
23
25
|
def data
|
|
24
26
|
@mutex.synchronize do
|
|
25
27
|
@data ||= begin
|
|
26
|
-
ctime = File.ctime
|
|
27
|
-
if !ctime || ctime.to_date < ::Date.today || File.size(
|
|
28
|
+
ctime = File.ctime datafile if File.exist? datafile
|
|
29
|
+
if !ctime || ctime.to_date < ::Date.today || File.size(datafile).zero?
|
|
28
30
|
fetch_data(ctime)
|
|
29
31
|
end
|
|
30
32
|
unzip
|
|
@@ -32,22 +34,64 @@ module Relaton
|
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
#
|
|
38
|
+
# The cache file of the pubs-export zip. It lives under the same root as
|
|
39
|
+
# the relaton-index cache, so one setting moves both (for example to a
|
|
40
|
+
# writable directory on a read-only host).
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
#
|
|
44
|
+
def datafile
|
|
45
|
+
dir = Dir.home
|
|
46
|
+
dir = Relaton::Index.config.storage_dir if defined?(Relaton::Index)
|
|
47
|
+
File.expand_path "pubs-export.zip", File.join(dir, ".relaton", "nist")
|
|
48
|
+
end
|
|
49
|
+
|
|
35
50
|
private
|
|
36
51
|
|
|
37
52
|
#
|
|
38
|
-
# Fetch data form server and save it to file
|
|
53
|
+
# Fetch data form server and save it to file. When the cache file cannot
|
|
54
|
+
# be written (a read-only file system, such as AWS Lambda —
|
|
55
|
+
# relaton-nist#57), keep the downloaded zip in memory for this process.
|
|
39
56
|
#
|
|
40
57
|
# @param ctime [Time, nil] file creation time
|
|
41
58
|
#
|
|
42
59
|
def fetch_data(ctime)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
return unless !ctime || ctime < last_modified
|
|
61
|
+
|
|
62
|
+
@data = nil
|
|
63
|
+
zip = download
|
|
64
|
+
begin
|
|
65
|
+
write_datafile zip
|
|
66
|
+
@buffer = nil
|
|
67
|
+
rescue SystemCallError, IOError => e
|
|
68
|
+
Util.warn "Cannot write `#{datafile}` (#{e.message}); " \
|
|
69
|
+
"the CSRC pubs-export data stays in memory"
|
|
70
|
+
@buffer = zip
|
|
48
71
|
end
|
|
49
72
|
end
|
|
50
73
|
|
|
74
|
+
#
|
|
75
|
+
# Write the zip to a temp file and rename it into place, so a failed
|
|
76
|
+
# write (a full disk) never leaves a truncated cache that later
|
|
77
|
+
# processes would read as fresh.
|
|
78
|
+
#
|
|
79
|
+
# @param zip [String] zip file content
|
|
80
|
+
#
|
|
81
|
+
def write_datafile(zip)
|
|
82
|
+
FileUtils.mkdir_p File.dirname(datafile)
|
|
83
|
+
tmp = "#{datafile}.#{Process.pid}.tmp"
|
|
84
|
+
File.binwrite tmp, zip
|
|
85
|
+
File.rename tmp, datafile
|
|
86
|
+
ensure
|
|
87
|
+
FileUtils.rm_f tmp if tmp && File.exist?(tmp)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download
|
|
91
|
+
uri_open = URI.method(:open) || Kernel.method(:open)
|
|
92
|
+
uri_open.call("#{PUBS_EXPORT}.zip", &:read)
|
|
93
|
+
end
|
|
94
|
+
|
|
51
95
|
def last_modified
|
|
52
96
|
uri = URI("#{PUBS_EXPORT}.meta")
|
|
53
97
|
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
@@ -56,15 +100,29 @@ module Relaton
|
|
|
56
100
|
end
|
|
57
101
|
|
|
58
102
|
#
|
|
59
|
-
# upack zip file
|
|
103
|
+
# upack zip file (the in-memory copy when the cache was not writable)
|
|
60
104
|
#
|
|
61
105
|
# @return [Array<Hash>] json data
|
|
62
106
|
#
|
|
63
107
|
def unzip
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
if @buffer
|
|
109
|
+
# open_buffer returns the buffer, not the block's value
|
|
110
|
+
rows = nil
|
|
111
|
+
Zip::File.open_buffer(StringIO.new(@buffer)) do |zf|
|
|
112
|
+
rows = read_entry zf
|
|
113
|
+
end
|
|
114
|
+
@buffer = nil
|
|
115
|
+
rows
|
|
116
|
+
else
|
|
117
|
+
Zip::File.open(datafile) { |zf| read_entry zf }
|
|
66
118
|
end
|
|
67
119
|
end
|
|
120
|
+
|
|
121
|
+
# The block form closes the entry stream. Without it the cache file
|
|
122
|
+
# stays open, and Windows then refuses to replace or delete it.
|
|
123
|
+
def read_entry(zip_file)
|
|
124
|
+
Relaton::Json.parse_external zip_file.first.get_input_stream(&:read)
|
|
125
|
+
end
|
|
68
126
|
end
|
|
69
127
|
end
|
|
70
128
|
end
|
data/lib/relaton/ogc/CLAUDE.md
CHANGED
|
@@ -203,7 +203,7 @@ Models declare attributes and XML mappings in the same class. `Item` is the seri
|
|
|
203
203
|
- **RSpec** with `--format documentation`
|
|
204
204
|
- **VCR** + **WebMock** for HTTP interaction recording (`spec/vcr_cassettes/`)
|
|
205
205
|
- **Jing** for RelaxNG schema validation against `grammars/relaton-ogc-compile.rng`
|
|
206
|
-
- **
|
|
206
|
+
- **canon** for XML comparison in tests
|
|
207
207
|
- Fixtures live in `spec/fixtures/` (YAML, XML)
|
|
208
208
|
- Integration tests in `spec/relaton/ogc/integration_spec.rb` exercise the full search → fetch → serialize pipeline
|
|
209
209
|
|
data/lib/relaton/omg/CLAUDE.md
CHANGED
|
@@ -124,7 +124,7 @@ Items can be serialized to/from YAML and XML. Tests verify round-trip fidelity f
|
|
|
124
124
|
### Test patterns
|
|
125
125
|
|
|
126
126
|
- RSpec with `expect` syntax (monkey patching disabled)
|
|
127
|
-
- `
|
|
127
|
+
- `canon` for XML comparison
|
|
128
128
|
- `ruby-jing` for RELAX NG schema validation against `grammars/relaton-omg-compile.rng`
|
|
129
129
|
- VCR cassettes in `spec/vcr_cassettes/` for HTTP interaction recording
|
|
130
130
|
- Fixtures in `spec/fixtures/` (YAML and XML reference files)
|
|
@@ -114,7 +114,7 @@ RNG grammar files in `grammars/` define the XML schema. Tests validate fixtures
|
|
|
114
114
|
- **Index fixture:** `spec/fixtures/index-v2.zip` (pubid-backed, `_type: pubid:plateau:*` rows) is pre-loaded into the `Relaton::Index` pool in `before(:suite)` via a `Relaton::Index::Type` built with `::Pubid::Plateau::Identifier` (configured in `spec/support/webmock.rb`, mirroring NIST/CIE). Run `rake spec:update_index` to refresh from relaton-data-plateau.
|
|
115
115
|
- RSpec with `expect` syntax (no monkey-patching)
|
|
116
116
|
- VCR cassettes record HTTP interactions (in `spec/fixtures/vcr_cassettes/`)
|
|
117
|
-
- `
|
|
117
|
+
- `canon` for XML comparison assertions
|
|
118
118
|
- `jing` matcher for RNG schema validation
|
|
119
119
|
- SimpleCov for code coverage
|
|
120
120
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Render
|
|
5
|
+
# Compatibility entry point (relaton-render#90).
|
|
6
|
+
#
|
|
7
|
+
# The v2 engine replaced the liquid/config/i18n machinery with the
|
|
8
|
+
# template evaluator over CitationStyle instances
|
|
9
|
+
# (Relaton::Render::Iso690). isodoc main's render-isodoc subclasses
|
|
10
|
+
# this constant, so it must exist for all-git-main stacks to boot.
|
|
11
|
+
# This facade delegates rendering to the v2 engine; the retired
|
|
12
|
+
# options (config:, template overrides in liquid syntax) are ignored
|
|
13
|
+
# with one warning — migrate to a CitationStyle instance, which
|
|
14
|
+
# expresses everything the old config.yml did (see the citation
|
|
15
|
+
# module in relaton-models).
|
|
16
|
+
class General
|
|
17
|
+
# The eref `style=` vocabulary metanorma-standoc validates against
|
|
18
|
+
# (cleanup/xref.rb reads citetemplate.template_raw's keys) — the
|
|
19
|
+
# 1.x default config's top-level citation styles.
|
|
20
|
+
CITE_STYLES = %i[
|
|
21
|
+
author_date author_date_br author date reference_tag title
|
|
22
|
+
title_reference_tag short
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
CiteTemplate = Struct.new(:template_raw)
|
|
26
|
+
|
|
27
|
+
def initialize(options = {})
|
|
28
|
+
options = deep_symbolize(options)
|
|
29
|
+
@lang = options[:language] || options[:lang] || "en"
|
|
30
|
+
renderer_opts = { lang: @lang, script: options[:script] || "Latn",
|
|
31
|
+
labels: options[:i18nhash] || {} }
|
|
32
|
+
# A flavor names its own CitationStyle instance (name or YAML path)
|
|
33
|
+
options[:style] and renderer_opts[:style] = options[:style]
|
|
34
|
+
@renderer = Iso690::Renderer.new(**renderer_opts)
|
|
35
|
+
@renderer_opts = renderer_opts
|
|
36
|
+
@renderers_by_lang = {}
|
|
37
|
+
warn_general_config if options[:config]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A collection's items may each declare their own language; the
|
|
41
|
+
# renderer localizes by language, so a reference renders in its
|
|
42
|
+
# own language within a document of another (the 1.x plateau
|
|
43
|
+
# per-reference behaviour)
|
|
44
|
+
def renderer_for(lang)
|
|
45
|
+
return @renderer if lang.nil? || lang == @lang
|
|
46
|
+
|
|
47
|
+
@renderers_by_lang[lang] ||= Iso690::Renderer.new(
|
|
48
|
+
**@renderer_opts.merge(lang: lang,
|
|
49
|
+
script: %w(ja ko zh).include?(lang) ? nil : "Latn")
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render(model, embedded: false, **opts)
|
|
54
|
+
model = to_model(model)
|
|
55
|
+
text =
|
|
56
|
+
if model.respond_to?(:formattedref) && model.formattedref
|
|
57
|
+
model.formattedref.content
|
|
58
|
+
else
|
|
59
|
+
# a collection's items may each declare their own language;
|
|
60
|
+
# the single-item render routes like render_all does
|
|
61
|
+
renderer_for(Array(model.language).first).render(model, **opts)
|
|
62
|
+
end
|
|
63
|
+
embedded ? text : "<formattedref>#{text}</formattedref>"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def citation(model, **opts)
|
|
67
|
+
@renderer.citation(to_model(model), **opts)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# isodoc's references rendering: renderings[id][:formattedref]
|
|
71
|
+
# feeds the <formattedref> of every bibitem (isodoc
|
|
72
|
+
# presentation_function/refs.rb).
|
|
73
|
+
def render_all(bib, type: "author-date")
|
|
74
|
+
items = facade_bibitems(bib) or return nil
|
|
75
|
+
disambiguators = date_disambiguators(items)
|
|
76
|
+
items.each_with_object({}).with_index do |(item, m), i|
|
|
77
|
+
renderer = renderer_for(Array(item.language).first)
|
|
78
|
+
ref = begin
|
|
79
|
+
renderer.render(item, disambiguator: disambiguators[item.id])
|
|
80
|
+
rescue ::Relaton::Render::Unrenderable
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
m[item.id] = {
|
|
84
|
+
author: renderer.in_text_author(item),
|
|
85
|
+
date: renderer.disambiguated_date(item,
|
|
86
|
+
disambiguator:
|
|
87
|
+
disambiguators[item.id]),
|
|
88
|
+
citation: citation_renderings(item, ref, renderer,
|
|
89
|
+
type: type,
|
|
90
|
+
disambiguator:
|
|
91
|
+
disambiguators[item.id]),
|
|
92
|
+
formattedref: terminate_reference(ref, item),
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# isodoc's styled citations consume these keys: the default is the
|
|
98
|
+
# authoritative identifier; the short cite is the style's citation,
|
|
99
|
+
# falling back to the reference rendering (the 1.x short-cite)
|
|
100
|
+
FIRST_DELIM = '<span class="fmt-first-biblio-delim"/>'
|
|
101
|
+
|
|
102
|
+
# A creator list alone (flavors' document-history name forms)
|
|
103
|
+
def creator_names(item)
|
|
104
|
+
@renderer.render_creators(to_model(item))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# render_all feeds the bibliography list: a reference not ending
|
|
108
|
+
# in the biblio terminator takes one (the 1.x render1 behaviour;
|
|
109
|
+
# single-item render stays verbatim)
|
|
110
|
+
# The batch disambiguation suffixes ("2022a"): items sharing a
|
|
111
|
+
# principal-creator surname and a date take alphabetical
|
|
112
|
+
# suffixes in bibliography order (the 1.x
|
|
113
|
+
# disambig_author_date_citations)
|
|
114
|
+
def date_disambiguators(items)
|
|
115
|
+
keys = items.filter_map do |item|
|
|
116
|
+
begin
|
|
117
|
+
[item.id, @renderer.author_key(item),
|
|
118
|
+
@renderer.citation(item)]
|
|
119
|
+
rescue ::Relaton::Render::Unrenderable
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
groups = keys.group_by { |_, author, cite| [author, cite] }
|
|
124
|
+
suffixes = {}
|
|
125
|
+
groups.each_value do |group|
|
|
126
|
+
next if group.size < 2
|
|
127
|
+
|
|
128
|
+
group.each_with_index do |(id, _author, _cite), i|
|
|
129
|
+
suffixes[id] = ("a".ord + i).chr
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
suffixes
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The bibliography terminator; flavors override to suppress it
|
|
136
|
+
# (1.x use_terminator?), receiving the item it terminates. A
|
|
137
|
+
# reference closing on a footnote closes the sentence before it:
|
|
138
|
+
# the terminator lands ahead of the trailing fn.
|
|
139
|
+
FOOTNOTE_CLOSE = "</fn>".freeze
|
|
140
|
+
|
|
141
|
+
def terminate_reference(ref, _item = nil)
|
|
142
|
+
return ref if ref.empty? || ref.rstrip.end_with?(".")
|
|
143
|
+
|
|
144
|
+
if (fn = ref.rindex("<fn"))
|
|
145
|
+
head = ref[0...fn].rstrip
|
|
146
|
+
return ref if head.empty?
|
|
147
|
+
|
|
148
|
+
sep = head.end_with?(".") ? "" : "."
|
|
149
|
+
return "#{head}#{sep}#{ref[fn..]}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
"#{ref}."
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The style-keyed capsule ("citation" => { author_date: "..." })
|
|
156
|
+
# is what isodoc's styled erefs consume (their style attribute
|
|
157
|
+
# names the key)
|
|
158
|
+
def citation_renderings(item, ref, renderer, type: "author-date",
|
|
159
|
+
disambiguator: nil)
|
|
160
|
+
# A style declaring shortFromReference takes the reference form
|
|
161
|
+
# with the first-biblio marker (the 1.x citetemplate short)
|
|
162
|
+
short = if renderer.short_from_reference?
|
|
163
|
+
renderer.render_short(item, FIRST_DELIM)
|
|
164
|
+
else
|
|
165
|
+
short = renderer.citation(item)
|
|
166
|
+
short.empty? ? renderer.render_short(item, FIRST_DELIM) :
|
|
167
|
+
short
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
title = renderer.title_citation(item)
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
full: ref,
|
|
174
|
+
default: item.docidentifier.first&.content.to_s,
|
|
175
|
+
short: short,
|
|
176
|
+
author_date: renderer.citation(item,
|
|
177
|
+
disambiguator: disambiguator),
|
|
178
|
+
author_date_br: renderer.bracketed_date_citation(
|
|
179
|
+
item, disambiguator: disambiguator,
|
|
180
|
+
),
|
|
181
|
+
author: renderer.in_text_author(item),
|
|
182
|
+
date: renderer.disambiguated_date(item),
|
|
183
|
+
# reference_tag: no rendering — isodoc falls back to the
|
|
184
|
+
# biblio-tag anchor (the 1.x nil)
|
|
185
|
+
title: title,
|
|
186
|
+
title_reference_tag: title,
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# isodoc's pref_ref_code_parse reads
|
|
191
|
+
# data[:authoritative_identifier] to build biblio-tag identifiers
|
|
192
|
+
# (presentation_function/docid.rb). Returns [data, self] as the
|
|
193
|
+
# 1.x signature did.
|
|
194
|
+
def parse(doc)
|
|
195
|
+
[{ authoritative_identifier: facade_docids(doc) }, self]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def citetemplate
|
|
199
|
+
CiteTemplate.new(CITE_STYLES.to_h { |s| [s, ""] })
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# 1.x accepted a <references> XML string (isodoc strips namespaces
|
|
205
|
+
# before building it) or a ready array of models.
|
|
206
|
+
def facade_bibitems(bib)
|
|
207
|
+
case bib
|
|
208
|
+
when Array then bib
|
|
209
|
+
when String
|
|
210
|
+
require "moxml"
|
|
211
|
+
root = Moxml.parse(bib).root
|
|
212
|
+
return nil unless root&.name == "references"
|
|
213
|
+
|
|
214
|
+
klass = facade_bibitem_class
|
|
215
|
+
root.xpath("./bibitem").filter_map { |b| klass.from_xml(b.to_xml) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def facade_bibitem_class
|
|
220
|
+
Relaton::Bib::Bibitem
|
|
221
|
+
rescue NameError
|
|
222
|
+
Relaton::Bib::Item
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# 1.x Parse#authoritative_identifier semantics: the
|
|
226
|
+
# primary/language cascade, the excluded types (URN, DOI, ISBN...)
|
|
227
|
+
# dropped, and scoped ids (biblio-tag duplicates, IEEE trademark)
|
|
228
|
+
# demoted within their type group. isodoc hands the live bibitem
|
|
229
|
+
# node with the document's default namespace still attached, so the
|
|
230
|
+
# element queries must be namespace-agnostic.
|
|
231
|
+
EXCLUDED_ID_TYPES = %w(METANORMA METANORMA-ORDINAL AUTHOR-DATE TITLE
|
|
232
|
+
URN ISO-REFERENCE ISSN ISBN DOI).freeze
|
|
233
|
+
|
|
234
|
+
# Flavors extend the authoritative-identifier exclusions by
|
|
235
|
+
# overriding this reader on their facade subclass
|
|
236
|
+
def excluded_id_types
|
|
237
|
+
EXCLUDED_ID_TYPES
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def facade_docids(doc)
|
|
241
|
+
node = doc
|
|
242
|
+
unless node.respond_to?(:xpath)
|
|
243
|
+
require "moxml"
|
|
244
|
+
node = Moxml.parse(doc.to_s).root
|
|
245
|
+
end
|
|
246
|
+
return [] unless node
|
|
247
|
+
|
|
248
|
+
ids = node.xpath("./*[local-name() = 'docidentifier']")
|
|
249
|
+
ids = facade_scope_filter(ids)
|
|
250
|
+
out = nil
|
|
251
|
+
[
|
|
252
|
+
->(x) { x["language"] == @lang && x["primary"] },
|
|
253
|
+
->(x) { x["primary"] },
|
|
254
|
+
->(x) { x["language"] == @lang },
|
|
255
|
+
->(_x) { true },
|
|
256
|
+
].each do |p|
|
|
257
|
+
out = ids.select do |x|
|
|
258
|
+
p.call(x) && !excluded_id_types.include?(facade_id_type(x))
|
|
259
|
+
end
|
|
260
|
+
out.empty? or break
|
|
261
|
+
end
|
|
262
|
+
# esc-wrapped: isodoc's l10n skips the tags, leaving a standard
|
|
263
|
+
# identifier's punctuation verbatim (the 1.x Parse behaviour)
|
|
264
|
+
out.map { |x| "<esc>#{x.text.strip}</esc>" }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def facade_id_type(id)
|
|
268
|
+
t = id["type"] or return nil
|
|
269
|
+
m = /\A(ISBN|ISSN)\..*/i.match(t) or return t.upcase
|
|
270
|
+
m[1].upcase
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def facade_scope_filter(ids)
|
|
274
|
+
ids.detect { |i| i["scope"] } or return ids
|
|
275
|
+
ids.group_by { |i| i["type"] }.flat_map do |type, group|
|
|
276
|
+
grouped = group.group_by { |i| i["scope"] }
|
|
277
|
+
if type == "IEEE" then grouped["trademark"] || grouped[nil] || []
|
|
278
|
+
else grouped[nil] || []
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# 1.x accepted string- and symbol-keyed options alike (the old
|
|
284
|
+
# engine deep-symbolized via metanorma-utils); isodoc's bibrenderer
|
|
285
|
+
# and every flavor still pass string keys.
|
|
286
|
+
def deep_symbolize(value)
|
|
287
|
+
case value
|
|
288
|
+
# isodoc i18n hashes carry nil keys (an unlabeled punct slot)
|
|
289
|
+
when Hash then value.to_h { |k, v| [k&.to_sym, deep_symbolize(v)] }
|
|
290
|
+
when Array then value.map { |v| deep_symbolize(v) }
|
|
291
|
+
else value
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# 1.x render/citation accepted a bibitem XML string; the Iso690
|
|
296
|
+
# engine consumes model instances.
|
|
297
|
+
def to_model(model)
|
|
298
|
+
return model unless model.is_a?(String)
|
|
299
|
+
|
|
300
|
+
klass = facade_bibitem_class
|
|
301
|
+
klass.from_xml(model)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def warn_general_config
|
|
305
|
+
return if General.config_warned
|
|
306
|
+
|
|
307
|
+
General.config_warned = true
|
|
308
|
+
warn "relaton-render: Relaton::Render::General is a compatibility " \
|
|
309
|
+
"facade over the v2 engine; the liquid config option is " \
|
|
310
|
+
"retired and ignored. Author a CitationStyle instance " \
|
|
311
|
+
"(relaton-models citation module) for custom rendering."
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
class << self
|
|
315
|
+
attr_accessor :config_warned
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|