relaton-cli 1.4.0 → 1.7.pre3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +46 -0
- data/Gemfile +0 -1
- data/docs/README.adoc +202 -7
- data/lib/relaton/bibcollection.rb +53 -35
- data/lib/relaton/bibdata.rb +3 -1
- data/lib/relaton/cli.rb +71 -32
- data/lib/relaton/cli/base_convertor.rb +1 -1
- data/lib/relaton/cli/command.rb +123 -61
- data/lib/relaton/cli/full_text_search.rb +91 -0
- data/lib/relaton/cli/relaton_file.rb +1 -1
- data/lib/relaton/cli/subcommand_collection.rb +221 -0
- data/lib/relaton/cli/subcommand_db.rb +73 -0
- data/lib/relaton/cli/version.rb +1 -1
- data/lib/relaton/cli/xml_to_html_renderer.rb +14 -7
- data/lib/relaton/cli/yaml_convertor.rb +1 -1
- data/lib/relaton/element_finder.rb +5 -5
- data/relaton-cli.gemspec +7 -6
- data/templates/_index.liquid +9 -1
- metadata +34 -33
- data/.github/workflows/macos.yml +0 -32
- data/.github/workflows/ubuntu.yml +0 -32
- data/.github/workflows/windows.yml +0 -35
data/lib/relaton/cli.rb
CHANGED
@@ -3,53 +3,92 @@ require "relaton"
|
|
3
3
|
require_relative "cli/command"
|
4
4
|
|
5
5
|
module Relaton
|
6
|
-
def self.db
|
7
|
-
Cli.relaton
|
6
|
+
def self.db(dir = nil)
|
7
|
+
Cli.relaton dir
|
8
8
|
end
|
9
9
|
|
10
10
|
module Cli
|
11
11
|
class RelatonDb
|
12
12
|
include Singleton
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
DBCONF = "#{Dir.home}/.relaton/dbpath".freeze
|
15
|
+
|
16
|
+
# @param dir [String, nil]
|
17
|
+
# @return [Relaton::Db]
|
18
|
+
def db(dir)
|
19
|
+
if dir
|
20
|
+
File.write DBCONF, dir, encoding: "UTF-8"
|
21
|
+
@db = Relaton::Db.new dir, nil
|
22
|
+
else
|
23
|
+
@db ||= Relaton::Db.new dbpath, nil
|
24
|
+
end
|
16
25
|
end
|
17
|
-
end
|
18
26
|
|
19
|
-
|
20
|
-
Relaton::Cli::Command.start(arguments)
|
21
|
-
end
|
27
|
+
private
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def self.relaton
|
31
|
-
RelatonDb.instance.db
|
29
|
+
# @return [String] path to DB
|
30
|
+
def dbpath
|
31
|
+
if File.exist?(DBCONF)
|
32
|
+
File.read(DBCONF, encoding: "UTF-8")
|
33
|
+
else "#{Dir.home}/.relaton/cache"
|
34
|
+
end
|
35
|
+
end
|
32
36
|
end
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
class << self
|
39
|
+
def start(arguments)
|
40
|
+
Relaton::Cli::Command.start(arguments)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Relaton
|
44
|
+
#
|
45
|
+
# Based on current setup, we need to initiate a Db instance to
|
46
|
+
# register all of it's supported processor backends. To make it
|
47
|
+
# easier we have added it as a class method so we can use this
|
48
|
+
# whenever necessary.
|
49
|
+
#
|
50
|
+
# @param dir [String, nil]
|
51
|
+
# @return [Relaton::Db]
|
52
|
+
def relaton(dir)
|
53
|
+
RelatonDb.instance.db dir
|
41
54
|
end
|
42
|
-
end
|
43
55
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
56
|
+
# @param content [Nokogiri::XML::Document]
|
57
|
+
# @return [RelatonBib::BibliographicItem,
|
58
|
+
# RelatonIsoBib::IsoBibliongraphicItem]
|
59
|
+
def parse_xml(doc)
|
60
|
+
if (proc = Cli.processor(doc))
|
61
|
+
proc.from_xml(doc.to_s)
|
62
|
+
else
|
63
|
+
RelatonBib::XMLParser.from_xml(doc.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param doc [Nokogiri::XML::Element]
|
68
|
+
# @return [RelatonIso::Processor, RelatonIec::Processor,
|
69
|
+
# RelatonNist::Processor, RelatonIetf::Processot,
|
70
|
+
# RelatonItu::Processor, RelatonGb::Processor,
|
71
|
+
# RelatonOgc::Processor, RelatonCalconnect::Processor]
|
72
|
+
def processor(doc)
|
73
|
+
docid = doc.at "docidentifier"
|
74
|
+
proc = get_proc docid
|
50
75
|
return proc if proc
|
76
|
+
|
77
|
+
Relaton::Registry.instance.by_type(docid&.text&.match(/^\w+/)&.to_s)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# @param doc [Nokogiri::XML::Element]
|
83
|
+
# @return [RelatonIso::Processor, RelatonIec::Processor,
|
84
|
+
# RelatonNist::Processor, RelatonIetf::Processot,
|
85
|
+
# RelatonItu::Processor, RelatonGb::Processor,
|
86
|
+
# RelatonOgc::Processor, RelatonCalconnect::Processor]
|
87
|
+
def get_proc(docid)
|
88
|
+
return unless docid && docid[:type]
|
89
|
+
|
90
|
+
Relaton::Registry.instance.by_type(docid[:type])
|
51
91
|
end
|
52
|
-
Relaton::Registry.instance.by_type(docid&.text&.match(/^\w+/)&.to_s)
|
53
92
|
end
|
54
93
|
end
|
55
94
|
end
|
data/lib/relaton/cli/command.rb
CHANGED
@@ -1,93 +1,136 @@
|
|
1
1
|
require "relaton/cli/relaton_file"
|
2
2
|
require "relaton/cli/xml_convertor"
|
3
3
|
require "relaton/cli/yaml_convertor"
|
4
|
+
require "relaton/cli/subcommand_collection"
|
5
|
+
require "relaton/cli/subcommand_db"
|
4
6
|
require "fcntl"
|
5
7
|
|
6
8
|
module Relaton
|
7
9
|
module Cli
|
8
10
|
class Command < Thor
|
11
|
+
include Relaton::Cli
|
12
|
+
|
9
13
|
desc "fetch CODE", "Fetch Relaton XML for Standard identifier CODE"
|
10
|
-
option :type, aliases: :t,
|
11
|
-
|
12
|
-
option :
|
14
|
+
option :type, aliases: :t, desc: "Type of standard to "\
|
15
|
+
"get bibliographic entry for"
|
16
|
+
option :format, aliases: :f, desc: "Output format (xml, yaml, bibtex). "\
|
17
|
+
"Default xml."
|
18
|
+
option :year, aliases: :y, type: :numeric, desc: "Year the standard was "\
|
19
|
+
"published"
|
20
|
+
option :"all-parts", type: :boolean, desc: "Fetch all parts"
|
21
|
+
option :"keep-year", type: :boolean, desc: "Undated reference should "\
|
22
|
+
"return actual reference with year"
|
23
|
+
option :retries, aliases: :r, type: :numeric, desc: "Number of network "\
|
24
|
+
"retries. Default 1."
|
13
25
|
|
14
26
|
def fetch(code)
|
15
|
-
Relaton.db
|
16
|
-
io = IO.new(STDOUT.fcntl(::Fcntl::F_DUPFD), mode:
|
27
|
+
# Relaton.db
|
28
|
+
io = IO.new(STDOUT.fcntl(::Fcntl::F_DUPFD), mode: "w:UTF-8")
|
17
29
|
io.puts(fetch_document(code, options) || supported_type_message)
|
18
30
|
end
|
19
31
|
|
20
|
-
desc "extract Metanorma-XML-File / Directory Relaton-XML-Directory",
|
21
|
-
|
32
|
+
desc "extract Metanorma-XML-File / Directory Relaton-XML-Directory",
|
33
|
+
"Extract Relaton XML from Metanorma XML file / directory"
|
34
|
+
option :extension, aliases: :x, default: "rxl", desc: "File extension "\
|
35
|
+
"of Relaton XML files, defaults to 'rxl'"
|
22
36
|
|
23
37
|
def extract(source_dir, outdir)
|
24
38
|
Relaton::Cli::RelatonFile.extract(source_dir, outdir, options)
|
25
39
|
end
|
26
40
|
|
27
|
-
desc "concatenate SOURCE-DIR COLLECTION-FILE", "Concatenate entries in
|
41
|
+
desc "concatenate SOURCE-DIR COLLECTION-FILE", "Concatenate entries in "\
|
42
|
+
"DIRECTORY (containing Relaton-XML or YAML) into a Relaton Collection"
|
28
43
|
option :title, aliases: :t, desc: "Title of resulting Relaton collection"
|
29
|
-
option :organization, aliases: :g, desc: "Organization owner of Relaton
|
30
|
-
|
31
|
-
option :
|
44
|
+
option :organization, aliases: :g, desc: "Organization owner of Relaton "\
|
45
|
+
"collection"
|
46
|
+
# option :new, aliases: :n, type: :boolean, desc: "Use the new Relaton "\
|
47
|
+
# "YAML format"
|
48
|
+
option :extension, aliases: :x, desc: "File extension of destination "\
|
49
|
+
"Relaton file, defaults to 'rxl'"
|
32
50
|
|
33
51
|
def concatenate(source_dir, outfile)
|
34
52
|
Relaton::Cli::RelatonFile.concatenate(source_dir, outfile, options)
|
35
53
|
end
|
36
54
|
|
37
|
-
desc "split Relaton-Collection-File Relaton-XML-Directory", "Split a
|
38
|
-
|
39
|
-
option :
|
55
|
+
desc "split Relaton-Collection-File Relaton-XML-Directory", "Split a "\
|
56
|
+
"Relaton Collection into multiple files"
|
57
|
+
option :extension, aliases: :x, default: "rxl", desc: "File extension "\
|
58
|
+
"of Relaton XML files, defaults to 'rxl'"
|
59
|
+
# option :new, aliases: :n, type: :boolean, desc: "Use the new Relaton "\
|
60
|
+
# "YAML format"
|
40
61
|
|
41
62
|
def split(source, outdir)
|
42
63
|
Relaton::Cli::RelatonFile.split(source, outdir, options)
|
43
64
|
end
|
44
65
|
|
45
|
-
desc "yaml2xml YAML", "Convert Relaton YAML into Relaton Collection XML
|
46
|
-
|
47
|
-
option :
|
48
|
-
|
49
|
-
option :
|
50
|
-
|
66
|
+
desc "yaml2xml YAML", "Convert Relaton YAML into Relaton Collection XML "\
|
67
|
+
"or separate files"
|
68
|
+
option :extension, aliases: :x, default: "rxl", desc: "File extension "\
|
69
|
+
"of Relaton XML files, defaults to 'rxl'"
|
70
|
+
option :prefix, aliases: :p, desc: "Filename prefix of individual "\
|
71
|
+
"Relaton XML files, defaults to empty"
|
72
|
+
option :outdir, aliases: :o, desc: "Output to the specified directory "\
|
73
|
+
"with individual Relaton Bibdata XML files"
|
74
|
+
option :require, aliases: :r, type: :array, desc: "Require LIBRARY "\
|
75
|
+
"prior to execution"
|
76
|
+
option :overwrite, aliases: :f, type: :boolean, default: false,
|
77
|
+
desc: "Overwrite the existing file"
|
51
78
|
|
52
79
|
def yaml2xml(filename)
|
53
80
|
Relaton::Cli::YAMLConvertor.to_xml(filename, options)
|
54
81
|
end
|
55
82
|
|
56
|
-
desc "xml2yaml XML", "Convert Relaton XML into Relaton Bibdata /
|
57
|
-
|
58
|
-
option :
|
59
|
-
|
60
|
-
option :
|
61
|
-
|
83
|
+
desc "xml2yaml XML", "Convert Relaton XML into Relaton Bibdata / "\
|
84
|
+
"Bibcollection YAML (and separate files)"
|
85
|
+
option :extension, aliases: :x, default: "yaml", desc: "File extension "\
|
86
|
+
"of Relaton YAML files, defaults to 'yaml'"
|
87
|
+
option :prefix, aliases: :p, desc: "Filename prefix of Relaton XML "\
|
88
|
+
"files, defaults to empty"
|
89
|
+
option :outdir, aliases: :o, desc: "Output to the specified directory "\
|
90
|
+
"with individual Relaton Bibdata YAML files"
|
91
|
+
option :require, aliases: :r, type: :array, desc: "Require LIBRARY "\
|
92
|
+
"prior to execution"
|
93
|
+
option :overwrite, aliases: :f, type: :boolean, default: false,
|
94
|
+
desc: "Overwrite the existing file"
|
62
95
|
|
63
96
|
def xml2yaml(filename)
|
64
97
|
Relaton::Cli::XMLConvertor.to_yaml(filename, options)
|
65
98
|
end
|
66
99
|
|
67
|
-
desc "xml2html RELATON-INDEX-XML", "Convert Relaton Collection XML into
|
68
|
-
|
69
|
-
option :
|
70
|
-
|
100
|
+
desc "xml2html RELATON-INDEX-XML", "Convert Relaton Collection XML into "\
|
101
|
+
"HTML"
|
102
|
+
option :stylesheet, aliases: :s, desc: "Stylesheet file path for "\
|
103
|
+
"rendering HTML index"
|
104
|
+
option :templatedir, aliases: :t, desc: "Liquid template directory for "\
|
105
|
+
"rendering Relaton items and collection"
|
106
|
+
option :overwrite, aliases: :f, type: :boolean, default: false,
|
107
|
+
desc: "Overwrite the existing file"
|
71
108
|
|
72
109
|
def xml2html(file, style = nil, template = nil)
|
73
110
|
Relaton::Cli::XMLConvertor.to_html(file, style, template)
|
74
111
|
end
|
75
112
|
|
76
|
-
desc "yaml2html RELATON-INDEX-YAML", "Concatenate Relaton Collection
|
77
|
-
|
78
|
-
option :
|
79
|
-
|
113
|
+
desc "yaml2html RELATON-INDEX-YAML", "Concatenate Relaton Collection "\
|
114
|
+
"YAML into HTML"
|
115
|
+
option :stylesheet, aliases: :s, desc: "Stylesheet file path for "\
|
116
|
+
"rendering HTML index"
|
117
|
+
option :templatedir, aliases: :t, desc: "Liquid template directory for "\
|
118
|
+
"rendering Relaton items and collection"
|
119
|
+
option :overwrite, aliases: :f, type: :boolean, default: false,
|
120
|
+
desc: "Overwrite the existing file"
|
80
121
|
|
81
122
|
def yaml2html(file, style = nil, template = nil)
|
82
123
|
Relaton::Cli::YAMLConvertor.to_html(file, style, template)
|
83
124
|
end
|
84
125
|
|
85
126
|
desc "convert XML", "Convert Relaton XML document"
|
86
|
-
option :format, aliases: :f, required: true, desc: "Output format
|
127
|
+
option :format, aliases: :f, required: true, desc: "Output format "\
|
128
|
+
"(yaml, bibtex, asciibib)"
|
87
129
|
option :output, aliases: :o, desc: "Output to the specified file"
|
88
130
|
|
89
|
-
def convert(file)
|
90
|
-
|
131
|
+
def convert(file) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
132
|
+
xml = Nokogiri::XML(File.read(file, encoding: "UTF-8"))
|
133
|
+
item = Relaton::Cli.parse_xml xml
|
91
134
|
result = if /yaml|yml/.match?(options[:format])
|
92
135
|
item.to_hash.to_yaml
|
93
136
|
else item.send "to_#{options[:format]}"
|
@@ -101,34 +144,53 @@ module Relaton
|
|
101
144
|
File.write output, result, encoding: "UTF-8"
|
102
145
|
end
|
103
146
|
|
104
|
-
|
105
|
-
|
106
|
-
# @param code [String]
|
107
|
-
# @param options [Hash]
|
108
|
-
# @option options [String] :type
|
109
|
-
# @option options [String, NilClass] :format
|
110
|
-
# @option options [Integer, NilClass] :year
|
111
|
-
def fetch_document(code, options)
|
112
|
-
if registered_types.include?(options[:type])
|
113
|
-
doc = Cli.relaton.fetch(code, options[:year]&.to_s)
|
114
|
-
if doc
|
115
|
-
options[:format] == "bibtex" ? doc.to_bibtex : doc.to_xml
|
116
|
-
else
|
117
|
-
"No matching bibliographic entry found"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
rescue RelatonBib::RequestError => e
|
121
|
-
e.message
|
122
|
-
end
|
147
|
+
desc "collection SUBCOMMAND", "Collection manipulations"
|
148
|
+
subcommand "collection", SubcommandCollection
|
123
149
|
|
124
|
-
|
125
|
-
|
150
|
+
desc "db SUBCOMMAND", "Cache DB manipulation"
|
151
|
+
subcommand "db", SubcommandDb
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# @param code [String]
|
157
|
+
# @param options [Hash]
|
158
|
+
# @option options [String] :type
|
159
|
+
# @option options [String, NilClass] :format
|
160
|
+
# @option options [Integer, NilClass] :year
|
161
|
+
# @return [String, nil]
|
162
|
+
def fetch_document(code, options) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize
|
163
|
+
year = options[:year]&.to_s
|
164
|
+
if (processor = Relaton::Registry.instance.by_type options[:type]&.upcase)
|
165
|
+
doc = Relaton.db.fetch_std code, year, processor.short, options.dup
|
166
|
+
elsif options[:type] then return
|
167
|
+
else doc = Relaton.db.fetch(code, year, options.dup)
|
126
168
|
end
|
169
|
+
return "No matching bibliographic entry found" unless doc
|
170
|
+
|
171
|
+
serialize doc, options[:format]
|
172
|
+
rescue RelatonBib::RequestError => e
|
173
|
+
e.message
|
174
|
+
end
|
127
175
|
|
128
|
-
|
129
|
-
|
130
|
-
|
176
|
+
# @param doc [RelatonBib::BibliographicItem]
|
177
|
+
# @param format [String]
|
178
|
+
# @return [String]
|
179
|
+
def serialize(doc, format)
|
180
|
+
case format
|
181
|
+
when "yaml", "yml" then doc.to_hash.to_yaml
|
182
|
+
when "bibtex" then doc.to_bibtex
|
183
|
+
else doc.to_xml
|
131
184
|
end
|
132
185
|
end
|
186
|
+
|
187
|
+
def supported_type_message
|
188
|
+
["Recognised types:", registered_types.sort.join(", ")].join(" ")
|
189
|
+
end
|
190
|
+
|
191
|
+
def registered_types
|
192
|
+
@registered_types ||=
|
193
|
+
Relaton::Registry.instance.processors.each.map { |_n, pr| pr.prefix }
|
194
|
+
end
|
133
195
|
end
|
134
196
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# require "forwardable"
|
2
|
+
|
3
|
+
module Relaton
|
4
|
+
class FullTextSeatch
|
5
|
+
# extend Forwardable
|
6
|
+
|
7
|
+
# def_delegators :@collections, :<<
|
8
|
+
|
9
|
+
# @return Regexp
|
10
|
+
attr_reader :regex
|
11
|
+
|
12
|
+
# @param collection [Relaton::Bibcollection]
|
13
|
+
def initialize(collection)
|
14
|
+
@collection = collection
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param text [String]
|
18
|
+
# @return [Array<Hash>]
|
19
|
+
def search(text)
|
20
|
+
@regex = %{(.*?)(.{0,20})(#{text})(.{0,20})(.*)}
|
21
|
+
@matches = @collection.items.reduce({}) do |m, item|
|
22
|
+
# m + results(col, rgx)
|
23
|
+
res = result item
|
24
|
+
m[item.id] = res if res.any?
|
25
|
+
m
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_results
|
30
|
+
@matches.each do |docid, attrs|
|
31
|
+
puts " Document ID: #{docid}"
|
32
|
+
print_attrs attrs, 4
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
def any?
|
38
|
+
@matches.any?
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
44
|
+
def print_attrs(attrs, indent)
|
45
|
+
ind = " " * indent
|
46
|
+
if attrs.is_a? String then puts ind + attrs
|
47
|
+
elsif attrs.is_a? Hash
|
48
|
+
attrs.each do |key, val|
|
49
|
+
pref = "#{ind}#{key}:"
|
50
|
+
if val.is_a? String then puts pref + " " + val
|
51
|
+
else
|
52
|
+
puts pref
|
53
|
+
print_attrs val, indent + 2
|
54
|
+
end
|
55
|
+
end
|
56
|
+
elsif attrs.is_a? Array then attrs.each { |v| print_attrs v, indent + 2 }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param item [Relaton::Bibdata]
|
61
|
+
# @return [Hash]
|
62
|
+
def result(item)
|
63
|
+
if item.is_a? String
|
64
|
+
message $~ if item.match regex
|
65
|
+
elsif item.respond_to? :reduce
|
66
|
+
item.reduce([]) do |m, i|
|
67
|
+
res = result i
|
68
|
+
m << res if res && !res.empty?
|
69
|
+
m
|
70
|
+
end
|
71
|
+
else
|
72
|
+
item.instance_variables.reduce({}) do |m, var|
|
73
|
+
res = result item.instance_variable_get(var)
|
74
|
+
m[var.to_s.tr(":@", "")] = res if res && !res.empty?
|
75
|
+
m
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
80
|
+
|
81
|
+
# @param match [MatchData]
|
82
|
+
# @return [String]
|
83
|
+
def message(match)
|
84
|
+
msg = ""
|
85
|
+
msg += "..." unless match[1].empty?
|
86
|
+
msg += "#{match[2]}\e[4m#{match[3]}\e[24m#{match[4]}"
|
87
|
+
msg += "..." unless match[5].empty?
|
88
|
+
msg
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|