relaton 3.0.7 → 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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# relaton-cli ships in lockstep with the combined `relaton` gem, so its version
|
|
2
|
+
# is that gem's version — the single source of truth in Relaton::VERSION. relaton
|
|
3
|
+
# is a hard runtime dependency, so requiring its lightweight version file always
|
|
4
|
+
# resolves. Don't hardcode a string here; it would silently drift on a bump.
|
|
5
|
+
require "relaton/version"
|
|
6
|
+
|
|
7
|
+
module Relaton
|
|
8
|
+
module Cli
|
|
9
|
+
VERSION = Relaton::VERSION
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "relaton/cli/base_convertor"
|
|
3
|
+
|
|
4
|
+
module Relaton
|
|
5
|
+
module Cli
|
|
6
|
+
class XMLConvertor < Relaton::Cli::BaseConvertor
|
|
7
|
+
def to_yaml
|
|
8
|
+
convert_and_write(file_content, :to_yaml)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Convert to YAML
|
|
12
|
+
#
|
|
13
|
+
# This interface allow us to convert any XML file to YAML.
|
|
14
|
+
# It only require us to provide a valid XML file and it can
|
|
15
|
+
# do converstion using default attributes, but it also allow
|
|
16
|
+
# us to provide custom options to customize this converstion
|
|
17
|
+
# process.
|
|
18
|
+
#
|
|
19
|
+
# @param file [File] The complete path to a XML file
|
|
20
|
+
# @param options [Hash] Options as hash key, value pairs.
|
|
21
|
+
#
|
|
22
|
+
def self.to_yaml(file, options = {})
|
|
23
|
+
new(file, options).to_yaml
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def default_ext
|
|
29
|
+
"yaml"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param content [Nokogiri::XML::Document]
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def convert_content(content)
|
|
35
|
+
if %w[bibitem bibdata].include? content.root.name
|
|
36
|
+
Bibdata.from_xml(content)
|
|
37
|
+
else
|
|
38
|
+
Bibcollection.from_xml(content)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def file_content
|
|
43
|
+
Nokogiri::XML(File.read(file, encoding: "utf-8"))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "liquid"
|
|
3
|
+
|
|
4
|
+
module Relaton::Cli
|
|
5
|
+
class XmlToHtmlRenderer
|
|
6
|
+
def initialize(liquid_dir: nil, stylesheet: nil)
|
|
7
|
+
@liquid_dir = liquid_dir
|
|
8
|
+
@stylesheet = read_file(stylesheet)
|
|
9
|
+
init_liquid_template_and_filesystem
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param index_xml [String] Relaton XML
|
|
13
|
+
# @return [String] HTML
|
|
14
|
+
def render(index_xml)
|
|
15
|
+
Liquid::Template
|
|
16
|
+
.parse(template)
|
|
17
|
+
.render(build_liquid_document(index_xml))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def uri_for_extension(uri, extension)
|
|
21
|
+
uri&.sub(/\.[^.]+$/, ".#{extension}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Render HTML
|
|
25
|
+
#
|
|
26
|
+
# This interface allow us to convert a Relaton XML to HTML
|
|
27
|
+
# using the specified liquid template and stylesheets. It
|
|
28
|
+
# also do some minor clean during this conversion.
|
|
29
|
+
#
|
|
30
|
+
# @param file [String] Relaton XML
|
|
31
|
+
# @param options [Hash]
|
|
32
|
+
# @return [String] HTML
|
|
33
|
+
def self.render(file, options)
|
|
34
|
+
new(**options).render(file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :stylesheet, :liquid_dir, :template
|
|
40
|
+
|
|
41
|
+
def read_file(file)
|
|
42
|
+
File.read(file, encoding: "utf-8")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# rubocop:disable Metrics/MethodLength
|
|
46
|
+
# @param source [String] Relaton XML
|
|
47
|
+
def build_liquid_document(source)
|
|
48
|
+
bibcollection = build_bibcollection(source)
|
|
49
|
+
hash_to_liquid(
|
|
50
|
+
depth: 2,
|
|
51
|
+
css: stylesheet,
|
|
52
|
+
title: bibcollection.title,
|
|
53
|
+
date: DateTime.now.to_s,
|
|
54
|
+
author: bibcollection.author,
|
|
55
|
+
documents: document_items(bibcollection)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def init_liquid_template_and_filesystem
|
|
60
|
+
file_system = Liquid::LocalFileSystem.new(liquid_dir)
|
|
61
|
+
@template = read_file(file_system.full_path("index"))
|
|
62
|
+
|
|
63
|
+
Liquid::Template.file_system = file_system
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
67
|
+
|
|
68
|
+
# TODO: This should be recursive, but it's not
|
|
69
|
+
# @param hash [Hash]
|
|
70
|
+
# @option hash [Integer] :dept
|
|
71
|
+
# @option hash [String] :css path to stylesheet file
|
|
72
|
+
# @option hash [String] :title
|
|
73
|
+
# @option hash [String] :author
|
|
74
|
+
# @option hash [Array<Hash>] :documents
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash]
|
|
77
|
+
def hash_to_liquid(hash)
|
|
78
|
+
hash.map do |key, value|
|
|
79
|
+
case key
|
|
80
|
+
when "title"
|
|
81
|
+
if value.is_a?(Array)
|
|
82
|
+
title = value.detect { |t| t["type"] == "main" } || value.first
|
|
83
|
+
v = title ? title["content"] : nil
|
|
84
|
+
elsif value.is_a?(Hash) then v = value["content"]
|
|
85
|
+
else v = value
|
|
86
|
+
end
|
|
87
|
+
when "docidentifier"
|
|
88
|
+
did = if value.is_a?(Array)
|
|
89
|
+
value.detect { |d| (d["id"] || d["content"]).to_s !~ /^(http|https):\/\// } ||
|
|
90
|
+
value.first
|
|
91
|
+
else value
|
|
92
|
+
end
|
|
93
|
+
v = did
|
|
94
|
+
else v = value
|
|
95
|
+
end
|
|
96
|
+
[key.to_s, empty2nil(v)]
|
|
97
|
+
end.to_h
|
|
98
|
+
end
|
|
99
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
100
|
+
|
|
101
|
+
def empty2nil(value)
|
|
102
|
+
value unless value.is_a?(String) && value.empty? && !value.nil?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param source [String] Relaton XML
|
|
106
|
+
# @return [Relaton::Bibcollection]
|
|
107
|
+
def build_bibcollection(source)
|
|
108
|
+
Relaton::Bibcollection.from_xml(Nokogiri::XML(source))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param bibcollection [Relaton::Bibcollection]
|
|
112
|
+
# @return [Array<Hash>]
|
|
113
|
+
def document_items(bibcollection)
|
|
114
|
+
bibcollection.to_h["root"]["items"].map { |item| hash_to_liquid(item) }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "relaton/cli/base_convertor"
|
|
3
|
+
require "relaton/bib"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Cli
|
|
7
|
+
class YAMLConvertor < Relaton::Cli::BaseConvertor
|
|
8
|
+
def to_xml
|
|
9
|
+
if writable
|
|
10
|
+
convert_and_write(file_content, :to_xml)
|
|
11
|
+
else
|
|
12
|
+
convert_content(file_content).to_xml date_format: :full, bibdata: true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Convert to XML
|
|
18
|
+
#
|
|
19
|
+
# This interface allow us to convert any YAML file to XML.
|
|
20
|
+
# It only require us to provide a valid YAML file and it can
|
|
21
|
+
# do converstion using default attributes, but it also allow
|
|
22
|
+
# us to provide custom options to customize this converstion
|
|
23
|
+
# process.
|
|
24
|
+
#
|
|
25
|
+
# @param file [File] The complete path to a YAML file
|
|
26
|
+
# @param options [Hash] Options as hash key, value pairs.
|
|
27
|
+
#
|
|
28
|
+
def to_xml(file, options = {})
|
|
29
|
+
new(file, options).to_xml
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param content [Hash] document in YAML format
|
|
33
|
+
# @return [RelatonBib::BibliographicItem,
|
|
34
|
+
# RelatonIso::IsoBiblioraphicItem]
|
|
35
|
+
def convert_single_file(content)
|
|
36
|
+
flavor = content.dig("ext", "flavor") || doctype(content["docidentifier"])
|
|
37
|
+
if (processor = Relaton::Db::Registry.instance.by_type(flavor))
|
|
38
|
+
begin
|
|
39
|
+
processor.from_yaml content.to_yaml
|
|
40
|
+
rescue RuntimeError
|
|
41
|
+
Relaton::Bib::Item.from_yaml(content.to_yaml)
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
Relaton::Bib::Item.from_yaml(content.to_yaml)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @param content [Hash]
|
|
51
|
+
# @return [String]
|
|
52
|
+
def doctype(docid)
|
|
53
|
+
did = docid.is_a?(Array) ? docid.fetch(0) : docid
|
|
54
|
+
return unless did
|
|
55
|
+
|
|
56
|
+
did["type"] || did.fetch("content")&.match(/^\w+/)&.to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def default_ext
|
|
63
|
+
"rxl"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def file_content
|
|
67
|
+
date_to_string(YAML.load_file(file))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def date_to_string(obj)
|
|
71
|
+
if obj.is_a? Hash
|
|
72
|
+
obj.reduce({}) do |memo, (k, v)|
|
|
73
|
+
memo[k] = date_to_string(v)
|
|
74
|
+
memo
|
|
75
|
+
end
|
|
76
|
+
elsif obj.is_a? Array
|
|
77
|
+
obj.reduce([]) { |memo, v| memo << date_to_string(v) }
|
|
78
|
+
else
|
|
79
|
+
obj.is_a?(Date) ? obj.to_s : obj
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def convert_collection(content)
|
|
84
|
+
if content.has_key?("root")
|
|
85
|
+
content["root"]["items"] = content["root"]["items"].map do |i|
|
|
86
|
+
# RelatonBib::HashConverter::hash_to_bib(i)
|
|
87
|
+
self.class.convert_single_file(i)
|
|
88
|
+
end
|
|
89
|
+
Relaton::Bibcollection.new(content["root"])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def xml_content(_raw_file)
|
|
94
|
+
convert_content(file_content).to_xml(date_format: :full, bibdata: true)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def convert_content(content)
|
|
98
|
+
convert_collection(content) || self.class.convert_single_file(content)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/relaton/cli.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "thor"
|
|
3
|
+
require "thor/hollaback"
|
|
4
|
+
require_relative "cli/version"
|
|
5
|
+
require_relative "cli/util"
|
|
6
|
+
require_relative "cli/command"
|
|
7
|
+
|
|
8
|
+
module Relaton
|
|
9
|
+
def self.db(dir = nil)
|
|
10
|
+
Cli.relaton dir
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Cli
|
|
14
|
+
class RelatonDb
|
|
15
|
+
include Singleton
|
|
16
|
+
|
|
17
|
+
DBCONF = "#{Dir.home}/.relaton/dbpath".freeze
|
|
18
|
+
|
|
19
|
+
# @param dir [String, nil]
|
|
20
|
+
# @return [Relaton::Db]
|
|
21
|
+
def db(dir)
|
|
22
|
+
if dir
|
|
23
|
+
FileUtils.mkdir_p File.dirname(DBCONF)
|
|
24
|
+
File.write DBCONF, dir, encoding: "UTF-8"
|
|
25
|
+
@db = Relaton::Db.new dir, nil
|
|
26
|
+
else
|
|
27
|
+
@db ||= Relaton::Db.new dbpath, nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @return [String] path to DB
|
|
34
|
+
def dbpath
|
|
35
|
+
if File.exist?(DBCONF)
|
|
36
|
+
File.read(DBCONF, encoding: "UTF-8")
|
|
37
|
+
else "#{Dir.home}/.relaton/cache"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
def version
|
|
44
|
+
require "relaton/bib"
|
|
45
|
+
registry = Relaton::Db::Registry.instance
|
|
46
|
+
# Every flavor now ships inside the single `relaton` gem, so they all
|
|
47
|
+
# report Relaton::VERSION rather than a separate gem version.
|
|
48
|
+
puts "CLI => #{Relaton::Cli::VERSION}"
|
|
49
|
+
puts "relaton => #{Relaton::VERSION}"
|
|
50
|
+
puts "relaton-bib => #{Relaton::VERSION}"
|
|
51
|
+
registry.processors.each_key do |k|
|
|
52
|
+
name = k.to_s.sub("_", "-")
|
|
53
|
+
puts "#{name} => #{Relaton::VERSION}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def start(arguments)
|
|
58
|
+
Relaton::Cli::Command.start(arguments)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Relaton
|
|
62
|
+
#
|
|
63
|
+
# Based on current setup, we need to initiate a Db instance to
|
|
64
|
+
# register all of it's supported processor backends. To make it
|
|
65
|
+
# easier we have added it as a class method so we can use this
|
|
66
|
+
# whenever necessary.
|
|
67
|
+
#
|
|
68
|
+
# @param dir [String, nil]
|
|
69
|
+
# @return [Relaton::Db]
|
|
70
|
+
def relaton(dir)
|
|
71
|
+
RelatonDb.instance.db dir
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param content [Nokogiri::XML::Document]
|
|
75
|
+
# @return [RelatonBib::BibliographicItem,
|
|
76
|
+
# RelatonIsoBib::IsoBibliongraphicItem]
|
|
77
|
+
def parse_xml(doc)
|
|
78
|
+
doc.remove_namespaces! if doc.respond_to?(:remove_namespaces!)
|
|
79
|
+
if (proc = Cli.processor(doc))
|
|
80
|
+
proc.from_xml(doc.to_s)
|
|
81
|
+
else
|
|
82
|
+
Relaton::Bib::Item.from_xml(doc.to_s) rescue nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param doc [Nokogiri::XML::Element]
|
|
87
|
+
# @return [RelatonIso::Processor, RelatonIec::Processor,
|
|
88
|
+
# RelatonNist::Processor, RelatonIetf::Processot,
|
|
89
|
+
# RelatonItu::Processor, RelatonGb::Processor,
|
|
90
|
+
# RelatonOgc::Processor, RelatonCalconnect::Processor]
|
|
91
|
+
def processor(doc)
|
|
92
|
+
docid = doc.at "docidentifier"
|
|
93
|
+
proc = get_proc docid
|
|
94
|
+
return proc if proc
|
|
95
|
+
|
|
96
|
+
Relaton::Db::Registry.instance.by_type(docid&.text&.match(/^\w+/)&.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# @param doc [Nokogiri::XML::Element]
|
|
102
|
+
# @return [RelatonIso::Processor, RelatonIec::Processor,
|
|
103
|
+
# RelatonNist::Processor, RelatonIetf::Processot,
|
|
104
|
+
# RelatonItu::Processor, RelatonGb::Processor,
|
|
105
|
+
# RelatonOgc::Processor, RelatonCalconnect::Processor]
|
|
106
|
+
def get_proc(docid)
|
|
107
|
+
return unless docid && docid[:type]
|
|
108
|
+
|
|
109
|
+
Relaton::Db::Registry.instance.by_type(docid[:type])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/relaton/doi/CLAUDE.md
CHANGED
|
@@ -38,7 +38,7 @@ Key classes in `lib/relaton/doi/`:
|
|
|
38
38
|
- **RSpec** with `expect` syntax only (monkey patching disabled)
|
|
39
39
|
- **VCR** cassettes in `spec/vcr_cassettes/` record Crossref HTTP responses and are never auto-rerecorded (see root `CLAUDE.md`). Because Crossref rate-limits, **re-record this suite alone** — under parallel `rake spec` it answers 429 and the empty body gets recorded as if it were data.
|
|
40
40
|
- **XML fixtures** in `spec/fixtures/` — expected output XML files. The `read_fixture` helper auto-substitutes today's date into `<fetched>` tags.
|
|
41
|
-
- **
|
|
41
|
+
- **canon** gem for XML comparison in integration tests
|
|
42
42
|
- Integration tests in `spec/relaton/doi_spec.rb` cover 40+ document types via VCR cassettes
|
|
43
43
|
- Unit tests in `spec/relaton/doi/parser_spec.rb` test Parser methods directly with hash inputs
|
|
44
44
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Relaton
|
|
2
|
+
module ElementFinder
|
|
3
|
+
attr_reader :document
|
|
4
|
+
|
|
5
|
+
def find_text(xpath, element = nil)
|
|
6
|
+
find(xpath, element)&.text
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def find_html(xpath, element = nil)
|
|
10
|
+
find(xpath, element)&.inner_html
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find(xpath, element = nil)
|
|
14
|
+
(element || document).at(apply_namespace(xpath))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find_xpath(xpath, element = nil)
|
|
18
|
+
element&.xpath(apply_namespace(xpath))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply_namespace(xpath)
|
|
22
|
+
xpath
|
|
23
|
+
.gsub(%r{/([a-zA-Z])}, "/xmlns:\\1")
|
|
24
|
+
.gsub(%r{::([a-zA-Z])}, "::xmlns:\\1")
|
|
25
|
+
.gsub(%r{\[([a-zA-Z][a-z0-9A-Z@/]* ?=)}, "[xmlns:\\1")
|
|
26
|
+
.gsub(%r{\[([a-zA-Z][a-z0-9A-Z@/]*\])}, "[xmlns:\\1")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/relaton/gb/CLAUDE.md
CHANGED
|
@@ -44,4 +44,4 @@ GB parses identifiers with `Pubid::Gb::Identifier`. It publishes no index, so it
|
|
|
44
44
|
|
|
45
45
|
## Testing
|
|
46
46
|
|
|
47
|
-
RSpec with WebMock + VCR (cassettes in `spec/vcr_cassettes/`). Network is blocked via WebMock. SimpleCov and
|
|
47
|
+
RSpec with WebMock + VCR (cassettes in `spec/vcr_cassettes/`). Network is blocked via WebMock. SimpleCov and canon are loaded via `spec/support/`. No index fixture — GB is scraped, not indexed; `spec/fixtures/` holds XML/YAML round-trip data.
|
data/lib/relaton/ietf/CLAUDE.md
CHANGED
|
@@ -375,7 +375,7 @@ and the affected files come through with their real characters —
|
|
|
375
375
|
- **An oracle for `read_bibxml` must read binary.** The method is byte-verbatim (`File.binread`), so comparing it against `File.read(path, encoding: "UTF-8")` compares two different strings **on Windows only**: Ruby opens a file in text mode there and collapses CRLF to LF. The repo declares no `.gitattributes` and the GitHub Windows runner image defaults to `core.autocrlf=true`, so a fixture stored with LF is checked out CRLF — which failed all three `windows-latest` jobs while every Linux and macOS job passed. Use `File.read(path, mode: "rb", encoding: "UTF-8")`. macOS cannot reproduce this by checking the fixture out as CRLF, because its text-mode read does not translate; emulate Windows explicitly with `File.read(path, encoding: "UTF-8", universal_newline: true)`. The two `keeps CRLF line endings verbatim` examples pin the contract with `binwrite` temp files, independent of the checkout — one per branch, since the CP1252 repair rebuilds the string run by run and is where a line ending is likeliest to be lost. Note that a byte-equality oracle alone cannot catch a dropped `force_encoding`: `rfc.xml` is pure ASCII, and Ruby compares two ASCII-only strings as equal across encodings, so the example asserts `encoding` separately.
|
|
376
376
|
- **Schema validation** via `ruby-jing` against the shared RNG grammars in the repo-root `grammar/` (`Jing.new "../../grammar/relaton-ietf-compile.rng"`, relative to the suite's CWD `spec/ietf/`)
|
|
377
377
|
- **Shared examples** for org/person parsing in `bibxml_parser_spec.rb` (`parse_org`, `parse_person`)
|
|
378
|
-
- **`
|
|
378
|
+
- **`canon`** gem used for XML comparison (`be_equivalent_to` matcher)
|
|
379
379
|
|
|
380
380
|
## RuboCop
|
|
381
381
|
|
data/lib/relaton/iho/CLAUDE.md
CHANGED
|
@@ -55,11 +55,11 @@ In `item.rb`, `require_relative "relation"` is placed **after** the `Item` class
|
|
|
55
55
|
|
|
56
56
|
`Bibliography.search`/`.get` fetches YAML from the relaton-data-iho GitHub repo via `Relaton::Index`, then deserializes into model objects.
|
|
57
57
|
|
|
58
|
-
**Editions and series.** The pubid
|
|
58
|
+
**Editions and series (the pubid 2.0.5 component model, relaton#440).** The pubid attribute is `edition` (dotted `a.b.c`, `IHO B-12 3.0.0`; the pre-440 name `version` is gone). A reference with no edition answers with the **latest**: `recency_key` compares the segments as integers (`2.10.0` after `2.9.0`). It used to be `min_by` on the string, which returned the oldest.
|
|
59
59
|
|
|
60
|
-
**
|
|
60
|
+
**Serial locators are volumes (relaton#440 unified the year locators of relaton#407).** The yearbook year IS a volume: `IHO P-5 Vol. 2020` is canonical, the bare-year faces (`P-5 2020`, `P-5/2020`) parse as legacy aliases into the same `volume`, and the IHR is `P-1 Vol. 30` with `Issue 1` (no parens) or the short slash form `P-1/30`. A bare series query answers with the latest volume/issue. **Components can publish separately and carry their own edition:** `IHO S-66 Suppl 1 2.0.0` gives `supplement_edition`, `S-100 Part S-102 1.0.0` gives `part_edition`, annexes likewise — and the publication years in parens (`S-52 Annex A 4.0.3 (2021)`) are `*_year` metadata, not identity: a stated year pins the record, an omitted one wildcards. Matching (`pubid_match?`) treats a discriminator the query omits as a wildcard: `IHO P-1 Vol. 30` matches any issue of volume 30. The comparison runs through pubid's `#exclude` — excluding `:volume` nils the `issue`, excluding an `:appendix` nils its `appendix_subnumber` (scalar clusters) — the way BIPM's `stem_keys` does. The subdivisions (part/appendix/annex/supplement) stay strict: this flavor cannot use the `===` subset match, because pubid does not declare `subset_strict` for IHO's subdivisions and a nil `part` would read as "any part" (`IHO S-100` would answer with a part row).
|
|
61
61
|
|
|
62
|
-
**The edition reaches the index only through the committed row.** A record prints `B-12` and keeps its edition in `edition:`, so `relaton index` cannot read a version from the docid. `MachineIndex#refine` (relaton-cli) takes the committed `index-v3.yaml` row when it adds only a key in `REFINABLE_KEYS` to the parse — `version`/`edition`/`volume`/`year`/`issue` (relaton#407 added the year/issue pair for the series rows). Before that, the Pages index (generated 2026-10-29) had no `version` in any of its 325 rows, so every versioned reference answered not-found (relaton-data-iho#20). The suite did not see it: `spec/fixtures/index-v3.zip` is the committed index, which has versions. The suite's fixture
|
|
62
|
+
**The edition reaches the index only through the committed row.** A record prints `B-12` and keeps its edition in `edition:`, so `relaton index` cannot read a version from the docid. `MachineIndex#refine` (relaton-cli) takes the committed `index-v3.yaml` row when it adds only a key in `REFINABLE_KEYS` to the parse — `version`/`edition`/`volume`/`year`/`issue` (relaton#407 added the year/issue pair for the series rows). Before that, the Pages index (generated 2026-10-29) had no `version` in any of its 325 rows, so every versioned reference answered not-found (relaton-data-iho#20). The suite did not see it: `spec/fixtures/index-v3.zip` is the committed index, which has versions. The suite's fixture carries the #48-era re-keyed series rows migrated to the #440 keys (`version`→`edition`/`<component>_edition`, year locators→`volume`; `support/migrate_index_fixture.rb` did the one-shot migration) — refresh the fixture from the live published zip once relaton-data-iho round 2 republishes, and reconcile any file-name drift then.
|
|
63
63
|
|
|
64
64
|
### Test Infrastructure
|
|
65
65
|
|
|
@@ -96,31 +96,37 @@ module Relaton
|
|
|
96
96
|
)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# The sort key for the latest record in a bucket (relaton#407
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# key
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
99
|
+
# The sort key for the latest record in a bucket (relaton#407,
|
|
100
|
+
# reshaped by relaton#440's component model). Volume-locator series
|
|
101
|
+
# (P-1, P-5) carry a `volume` with an optional `issue` — the
|
|
102
|
+
# yearbook year IS a volume (`P-5 Vol. 2020`; the bare-year faces
|
|
103
|
+
# of the #48 re-key parse as legacy aliases), so the year locators
|
|
104
|
+
# fold into the volume ordering. Edition-keyed products carry a
|
|
105
|
+
# dotted `edition` (`B-12 3.0.0`), and a component published
|
|
106
|
+
# separately carries its own edition (`S-66 Suppl 1 2.0.0` →
|
|
107
|
+
# `supplement_edition`). Every component compares numerically
|
|
108
|
+
# segment by segment (2.10.0 after 2.9.0); a row with no
|
|
109
|
+
# discriminator at all is the oldest.
|
|
109
110
|
def recency_key(id)
|
|
110
|
-
if id.
|
|
111
|
-
[id.year.to_i]
|
|
112
|
-
elsif id.volume
|
|
111
|
+
if id.volume
|
|
113
112
|
[id.volume.to_i, id.issue.to_i]
|
|
114
113
|
else
|
|
115
|
-
id.
|
|
114
|
+
ed = id.edition || id.supplement_edition || id.part_edition ||
|
|
115
|
+
id.annex_edition || id.appendix_edition || id.volume_edition
|
|
116
|
+
ed.to_s.split(".").map(&:to_i)
|
|
116
117
|
end
|
|
117
118
|
end
|
|
118
119
|
|
|
119
|
-
# The components
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
120
|
+
# The identity components beyond the series and subdivisions
|
|
121
|
+
# (relaton#440): the base `edition` — or the component-level
|
|
122
|
+
# edition when the component publishes separately — plus the
|
|
123
|
+
# volume/issue pair. The publication years (`year`, `*_year`) are
|
|
124
|
+
# metadata, not identity: a stated year pins the record, an
|
|
125
|
+
# omitted one wildcards it.
|
|
126
|
+
DISCRIMINATOR_KEYS = %i[
|
|
127
|
+
edition volume issue
|
|
128
|
+
supplement_edition part_edition annex_edition appendix_edition volume_edition
|
|
129
|
+
].freeze
|
|
124
130
|
|
|
125
131
|
# Both `row_id` and `query` are Pubid::Iho::Identifiers::* instances.
|
|
126
132
|
# Class identity pins the series (S/P/M/B/C). Subdivision keys
|
|
@@ -132,21 +138,29 @@ module Relaton
|
|
|
132
138
|
# so `IHO S-100` would answer with a part row.
|
|
133
139
|
#
|
|
134
140
|
# The discriminator keys stay nil-tolerant: an unqualified `IHO B-11`
|
|
135
|
-
# finds the latest edition, an unqualified `IHO P-5` the latest
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
+
# finds the latest edition, an unqualified `IHO P-5` the latest
|
|
142
|
+
# volume (the yearbook year — relaton#440 folded the year
|
|
143
|
+
# locators in), and `IHO P-1 Vol. 30` any issue of that volume —
|
|
144
|
+
# while a stated component must match exactly. The comparison runs
|
|
145
|
+
# through pubid's `#exclude` (the primitive `matches?` is built
|
|
146
|
+
# on), so the rules the flavor declares there ride along: excluding
|
|
147
|
+
# `:volume` nils the `issue` with it, and excluding an appendix
|
|
148
|
+
# nils its subnumber — the scalar clusters are one unit. `stem_keys`
|
|
149
|
+
# in the BIPM flavor is the same shape.
|
|
141
150
|
def pubid_match?(row_id, query)
|
|
142
151
|
row_id.class == query.class &&
|
|
143
152
|
row_id.exclude(*wildcards(query)) == query.exclude(*wildcards(query))
|
|
144
153
|
end
|
|
145
154
|
|
|
146
155
|
# The discriminators the query leaves unset: wildcards for the
|
|
147
|
-
# search, significant whenever the query states one
|
|
156
|
+
# search, significant whenever the query states one — plus the
|
|
157
|
+
# publication years, which are stated-metadata: pinned when the
|
|
158
|
+
# query carries them, wildcarded when it does not.
|
|
148
159
|
def wildcards(query)
|
|
149
|
-
|
|
160
|
+
keys = DISCRIMINATOR_KEYS + %i[
|
|
161
|
+
year supplement_year part_year annex_year appendix_year volume_year
|
|
162
|
+
]
|
|
163
|
+
keys.select { |key| query.public_send(key).nil? }
|
|
150
164
|
end
|
|
151
165
|
end
|
|
152
166
|
end
|
|
@@ -26,9 +26,10 @@ module Relaton
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# An IHO record prints `B-12` and keeps its edition in `edition`, so
|
|
29
|
-
# the edition goes on the key
|
|
29
|
+
# the edition goes on the key (relaton#408). The component model
|
|
30
|
+
# (relaton#440) renamed the pubid attribute `version` -> `edition`.
|
|
30
31
|
def item_cache_key(pubid, item)
|
|
31
|
-
with_components pubid,
|
|
32
|
+
with_components pubid, edition: item.edition&.content
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# @param xml [String]
|
data/lib/relaton/nist/CLAUDE.md
CHANGED
|
@@ -69,7 +69,8 @@ All data models inherit from `Lutaml::Model::Serializable` and use declarative a
|
|
|
69
69
|
- **`#write_file` serializes before it indexes**, so a record that fails to serialize leaves no index row naming a file that was never written. That is how a `Bib::Date` in `Scraper.fetch_dates` hid: `Nist::ItemData` takes `Nist::Date`, and YAML serialization rejects the parent class (the same strictness as the docidentifier note below).
|
|
70
70
|
- `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
|
|
71
71
|
- `Scraper` — fetches items from GitHub YAML or CSRC JSON
|
|
72
|
-
- `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates
|
|
72
|
+
- `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates. The cache file is `#datafile`, under `Relaton::Index.config.storage_dir` (default `Dir.home`; the `DATAFILE` constant is only that default).
|
|
73
|
+
- **A read-only file system must not stop a search (relaton-nist#57).** When the cache write fails, `#fetch_data` keeps the downloaded zip in `@buffer`, and `#unzip` reads it with `Zip::File.open_buffer`. In rubyzip 3, `open_buffer` returns the buffer, not the block value, so collect the rows in a local. The cache write goes to a temp file and is renamed into place (`#write_datafile`), so a failed write never leaves a truncated zip that later processes read as fresh. `HitCollection#search` also rescues the local-cache errors in `LOCAL_DATA_ERRORS` from `from_json` (`#from_json_or_empty`), logs a warning, and continues with `from_ga`. Do not widen that list to `SystemCallError`: `Errno::ECONNREFUSED` is one, and a CSRC outage would then answer "not found" from the index, which `Relaton::Db` can cache. Specs assert the warning with `output(...).to_stderr_from_any_process`. A `receive(:warn)` stub on `Util` is never hit: `Kernel#warn` is private, the stub stays private, and the explicit-receiver call goes to `method_missing`.
|
|
73
74
|
|
|
74
75
|
### Pubid-backed docidentifier
|
|
75
76
|
|
|
@@ -115,6 +116,15 @@ NIST-specific gotchas (why this isn't a straight copy of IEC/CCSDS):
|
|
|
115
116
|
numbered editions/revisions (`e2`, `r5`). Dates that ride the `update`
|
|
116
117
|
component (some NBS supplements) are left intact — `update` also encodes
|
|
117
118
|
non-date update codes (`/Upd2`), so stripping it would corrupt identity.
|
|
119
|
+
- **The item's `edition` comes from the DOI (relaton-nist#56).**
|
|
120
|
+
`ModsParser#parse_edition` sets `<edition number="N">N</edition>` only for a
|
|
121
|
+
numbered `e<N>` edition in the DOI (`NBS.CIRC.111e2` → 2, `11e2.1915` → 2):
|
|
122
|
+
250 of the 20,392 live MODS records. An `e<year>` edition is a date, and an
|
|
123
|
+
`r<N>` revision is not an edition, so both stay without one. The MODS
|
|
124
|
+
`<edition>` element is prose (`New version`, `Erratum`), never a number. The
|
|
125
|
+
issue's "edition 0 → 1" rule came from the old Crossref feed; it is kept for
|
|
126
|
+
a DOI `e0` (none in the live data). A DOI pubid cannot parse gives no
|
|
127
|
+
edition, not an error.
|
|
118
128
|
- **No `(all parts)` marker in `content`.** `content` is live-derived from
|
|
119
129
|
`@pubid`, so `to_all_parts!` freezes the already-stripped rendering into
|
|
120
130
|
`@raw_content` before wrapping `@pubid` in pubid's `AllParts` — matching
|
|
@@ -311,7 +311,13 @@ Relaton::Nist::Item.from_yaml File.read("spec/fixtures/item.yaml")
|
|
|
311
311
|
|
|
312
312
|
=== Fetching data
|
|
313
313
|
|
|
314
|
-
On a search, the CSRC data sources are fetched and stored in the user's cache
|
|
314
|
+
On a search, the CSRC data sources are fetched and stored in the user's cache,
|
|
315
|
+
`~/.relaton/nist/pubs-export.zip`. The cache follows
|
|
316
|
+
`Relaton::Index.config.storage_dir`, so one setting moves it together with the
|
|
317
|
+
index cache. When the cache cannot be written (for example on a read-only file
|
|
318
|
+
system such as AWS Lambda), the data stays in memory for the process. When the
|
|
319
|
+
CSRC data cannot be used at all, the search continues with the
|
|
320
|
+
`relaton-data-nist` index.
|
|
315
321
|
|
|
316
322
|
This gem uses the https://github.com/usnistgov/NIST-Tech-Pubs/releases/download/May2024/allrecords-MODS.xml dataset as NIST-Tech-Pubs data source.
|
|
317
323
|
|