relaton-render 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,188 @@
1
+ module Relaton
2
+ module Render
3
+ class Parse
4
+ def host(doc)
5
+ doc.relation.detect { |r| r.type == "includedIn" }&.bibitem
6
+ end
7
+
8
+ # TODO : first is naive choice
9
+ def title(doc)
10
+ return nil if doc.nil? || doc.title.empty?
11
+
12
+ t = doc.title.select { |x| x.title.language&.include? @lang }
13
+ t.empty? and t = doc.title
14
+ t.first&.title&.content
15
+ end
16
+
17
+ def medium(doc, host)
18
+ x = doc.medium || host&.medium or return nil
19
+
20
+ %w(content genre form carrier size scale).each_with_object({}) do |i, m|
21
+ m[i] = x.send i
22
+ end.compact
23
+ end
24
+
25
+ def size(doc)
26
+ x = doc.size or return nil
27
+ x.size.each_with_object({}) do |v, m|
28
+ m[v.type] ||= []
29
+ m[v.type] << v.value
30
+ end
31
+ end
32
+
33
+ def edition(doc, host)
34
+ doc.edition || host&.edition
35
+ end
36
+
37
+ def place(doc, host)
38
+ x = doc.place
39
+ x.empty? && host and x = host.place
40
+ x.empty? and return x
41
+ x.map(&:name)
42
+ end
43
+
44
+ def publisher(doc, host)
45
+ x = pick_contributor(doc, "publisher")
46
+ host and x ||= pick_contributor(host, "publisher")
47
+ x.nil? and return nil
48
+ x.map { |c| extractname(c) }
49
+ end
50
+
51
+ def distributor(doc, host)
52
+ x = pick_contributor(doc, "distributor")
53
+ host and x ||= pick_contributor(host, "distributor")
54
+ x.nil? and return nil
55
+ x.map { |c| extractname(c) }
56
+ end
57
+
58
+ def series(doc)
59
+ doc.series.detect { |s| s.type == "main" } ||
60
+ doc.series.detect { |s| s.type.nil? } ||
61
+ doc.series.first
62
+ end
63
+
64
+ def series_title(series, _doc)
65
+ return nil if series.nil?
66
+
67
+ series.title.respond_to?(:titles) and
68
+ return series.title.titles.first.title.content
69
+ series.title&.title&.content || series.formattedref&.content
70
+ end
71
+
72
+ def series_formatted(series, _doc)
73
+ series.formattedref&.content
74
+ end
75
+
76
+ def series_abbr(series, _doc)
77
+ series.abbreviation&.content
78
+ end
79
+
80
+ def series_num(series, _doc)
81
+ series.number
82
+ end
83
+
84
+ def series_partnumber(series, _doc)
85
+ series.partnumber
86
+ end
87
+
88
+ def series_run(series, _doc)
89
+ series.run
90
+ end
91
+
92
+ def standardidentifier(doc)
93
+ doc.docidentifier.each_with_object([]) do |id, ret|
94
+ ret << id.id unless standardidentifier_exclude.include? id.type
95
+ end
96
+ end
97
+
98
+ def standardidentifier_exclude
99
+ %w(metanorma metanorma-ordinal)
100
+ end
101
+
102
+ def uri(doc)
103
+ uri = nil
104
+ %i(doi uri src).each do |t|
105
+ uri = doc.link.detect { |u| u.type == t } and break
106
+ end
107
+ uri ||= doc.link.first
108
+ return nil unless uri
109
+
110
+ uri.content.to_s
111
+ end
112
+
113
+ def access_location(doc, host)
114
+ x = doc.accesslocation || host&.accesslocation or
115
+ return nil
116
+ x.first
117
+ end
118
+
119
+ def included(type)
120
+ ["article", "inbook", "incollection", "inproceedings"].include? type
121
+ end
122
+
123
+ def type(doc)
124
+ type = doc.type and return type
125
+ doc.relation.any? { |r| r.type == "includedIn" } and return "inbook"
126
+ "book"
127
+ end
128
+
129
+ def extent1(localities)
130
+ localities.each_with_object({}) do |l, ret|
131
+ ret[(l.type || "page").to_sym] = {
132
+ from: localized_string_or_text(l.reference_from),
133
+ to: localized_string_or_text(l.reference_to),
134
+ }
135
+ end
136
+ end
137
+
138
+ def extent(doc)
139
+ doc.extent.each_with_object([]) do |e, acc|
140
+ case e
141
+ when RelatonBib::LocalityStack
142
+ acc << extent1(e.locality)
143
+ when RelatonBib::Locality
144
+ acc << extent1(Array(e))
145
+ end
146
+ end
147
+ end
148
+
149
+ def draft(doc, host)
150
+ dr = doc.status&.stage&.value || host&.status&.stage&.value
151
+
152
+ { iteration: iter_ordinal(doc) || iter_ordinal(host), status: dr }
153
+ end
154
+
155
+ def iter_ordinal(doc)
156
+ return nil unless iter = doc&.status&.iteration
157
+
158
+ iter
159
+ end
160
+
161
+ def status(doc)
162
+ doc.status&.stage&.value
163
+ end
164
+
165
+ private
166
+
167
+ def blank?(text)
168
+ text.nil? || text.empty?
169
+ end
170
+
171
+ def pick_contributor(doc, role)
172
+ ret = doc.contributor.select do |c|
173
+ c.role.any? { |r| r.type == role }
174
+ end
175
+ ret.empty? ? nil : ret
176
+ end
177
+
178
+ def localized_string_or_text(str)
179
+ case str
180
+ when RelatonBib::LocalizedString
181
+ str.content
182
+ when String
183
+ str
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,15 @@
1
+ module Relaton
2
+ module Render
3
+ module Template
4
+ module CapitalizeFirst
5
+ def capitalize_first(words)
6
+ return nil if words.nil?
7
+
8
+ ret = words.split(/[ _]/)
9
+ ret.first.capitalize! if ret.size.positive?
10
+ ret.join("_")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,183 @@
1
+ require_relative "../utils/utils"
2
+ require_relative "liquid"
3
+
4
+ module Relaton
5
+ module Render
6
+ module Template
7
+ class General
8
+ def initialize(opt = {})
9
+ @htmlentities = HTMLEntities.new
10
+ customise_liquid
11
+ parse_options(opt)
12
+ end
13
+
14
+ def parse_options(opt)
15
+ opt = Utils::sym_keys(opt)
16
+ @i18n = opt[:i18n]
17
+ @template_raw = opt[:template].dup
18
+ @template =
19
+ case opt[:template]
20
+ when Hash
21
+ opt[:template].transform_values { |x| template_process(x) }
22
+ when Array then opt[:template].map { |x| template_process(x) }
23
+ else { default: template_process(opt[:template]) }
24
+ end
25
+ end
26
+
27
+ def customise_liquid
28
+ ::Liquid::Template
29
+ .register_filter(::Relaton::Render::Template::CapitalizeFirst)
30
+ end
31
+
32
+ # denote start and end of field,
33
+ # so that we can detect empty fields in postprocessing
34
+ FIELD_DELIM = "\u0018".freeze
35
+
36
+ # escape < >
37
+ LT_DELIM = "\u0019".freeze
38
+ GT_DELIM = "\u001a".freeze
39
+
40
+ # use tab internally for non-spacing delimiter
41
+ NON_SPACING_DELIM = "\t".freeze
42
+
43
+ def template_process(template)
44
+ t = template.gsub(/\{\{/, "#{FIELD_DELIM}{{")
45
+ .gsub(/\}\}/, "}}#{FIELD_DELIM}")
46
+ .gsub(/\t/, " ")
47
+ t1 = t.split(/(\{\{.+?\}\})/).map do |n|
48
+ n.include?("{{") ? n : n.gsub(/(?<!\\)\|/, "\t")
49
+ end.join
50
+ ::Liquid::Template.parse(t1)
51
+ end
52
+
53
+ def render(hash)
54
+ t = template_select(hash) or return nil
55
+
56
+ template_clean(t.render(liquid_hash(hash.merge("labels" => @i18n.get))))
57
+ end
58
+
59
+ def template_select(_hash)
60
+ @template[:default]
61
+ end
62
+
63
+ def template_clean(str)
64
+ str = str.gsub(/&#x3c;/i, LT_DELIM).gsub(/&#x3e;/i, GT_DELIM)
65
+ str = template_clean1(@htmlentities.decode(str))
66
+ /[[:alnum:]]/.match?(str) or return nil
67
+ str.strip.gsub(/#{LT_DELIM}/o, "&#x3c;")
68
+ .gsub(/#{GT_DELIM}/o, "&#x3e;")
69
+ .gsub(/&(?!#\S+?;)/, "&#x26;")
70
+ end
71
+
72
+ # use tab internally for non-spacing delimiter
73
+ def template_clean1(str)
74
+ str.gsub(/\S*#{FIELD_DELIM}#{FIELD_DELIM}\S*/o, "")
75
+ .gsub(/#{FIELD_DELIM}/o, "")
76
+ .gsub(/([,:;]\s*)+([,:;](\s|$))/, "\\2")
77
+ .gsub(/([,.:;]\s*)+([.](\s|$))/, "\\2")
78
+ .gsub(/([,:;]\s*)+(,(\s|$))/, "\\2")
79
+ .gsub(/(:\s+)(&\s)/, "\\2")
80
+ .gsub(/\s+([,.:;)])/, "\\1")
81
+ .sub(/^\s*[,.:;]\s*/, "")
82
+ .gsub(/_/, " ")
83
+ .gsub(/#{NON_SPACING_DELIM}/o, "").gsub(/\s+/, " ")
84
+ end
85
+
86
+ # need non-breaking spaces in fields: "Updated:_nil" ---
87
+ # we want the "Updated:" deleted,
88
+ # even if it's multiple words, as in French Mise_à_jour.
89
+ def liquid_hash(hash)
90
+ case hash
91
+ when Hash
92
+ hash.map { |k, v| [k.to_s, liquid_hash(v)] }.to_h
93
+ when Array
94
+ hash.map { |v| liquid_hash(v) }
95
+ when String
96
+ hash.empty? ? nil : hash.gsub(/ /, "_")
97
+ else hash
98
+ end
99
+ end
100
+ end
101
+
102
+ class Series < General
103
+ end
104
+
105
+ class Extent < General
106
+ def template_select(hash)
107
+ @template[hash[:type].to_sym]
108
+ end
109
+ end
110
+
111
+ class Size < General
112
+ def template_select(hash)
113
+ @template[hash[:type].to_sym]
114
+ end
115
+ end
116
+
117
+ class Name < General
118
+ def initialize(opt = {})
119
+ @etal_count = opt[:template]["etal_count"]
120
+ opt[:template].delete("etal_count")
121
+ super
122
+ end
123
+
124
+ def template_select(names)
125
+ return nil if names.nil? || names.empty?
126
+
127
+ case names[:surname].size
128
+ when 1 then @template[:one]
129
+ when 2 then @template[:two]
130
+ when 3 then @template[:more]
131
+ else template_select_etal(names)
132
+ end
133
+ end
134
+
135
+ def template_select_etal(names)
136
+ if @etal_count && names.size >= @etal_count
137
+ @template[:etal]
138
+ else expand_nametemplate(@template_raw[:more], names.size)
139
+ end
140
+ end
141
+
142
+ # assumes that template contains, consecutively and not interleaved,
143
+ # ...[0], ...[1], ...[2]
144
+ def expand_nametemplate(template, size)
145
+ t = nametemplate_split(template)
146
+ mid = (1..size - 2).each_with_object([]) do |i, m|
147
+ m << t[1].gsub(/\[1\]/, "[#{i}]")
148
+ end
149
+ template_process(t[0] + mid.join + t[2].gsub(/\[2\]/,
150
+ "[#{size - 1}]"))
151
+ end
152
+
153
+ def nametemplate_split(template)
154
+ curr = 0
155
+ prec = ""
156
+ t = template.split(/(\{[{%].+?[}%]\})/)
157
+ .each_with_object(["", "", ""]) do |n, m|
158
+ m, curr, prec = nametemplate_split1(n, m, curr, prec)
159
+
160
+ m
161
+ end
162
+ t[-1] += prec
163
+ t
164
+ end
165
+
166
+ def nametemplate_split1(elem, acc, curr, prec)
167
+ if match = /\{[{%].+?\[(\d)\]/.match(elem)
168
+ curr += 1 if match[1].to_i > curr
169
+ acc[curr] += prec
170
+ prec = ""
171
+ acc[curr] += elem
172
+ elsif /\{%\s*endif/.match?(elem)
173
+ acc[curr] += prec
174
+ prec = ""
175
+ acc[curr] += elem
176
+ else prec += elem
177
+ end
178
+ [acc, curr, prec]
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
File without changes
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Render
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.3.1".freeze
4
4
  end
5
5
  end
@@ -1,6 +1,7 @@
1
- require "relaton/version"
2
- require "relaton/render"
3
- require "parse/parse"
4
- require "template/template"
1
+ require "relaton/render/version"
2
+ require "relaton/render/general/render"
3
+ require "relaton/render/fields/fields"
4
+ require "relaton/render/parse/parse"
5
+ require "relaton/render/utils/utils"
5
6
  require "isodoc/i18n"
6
7
 
@@ -1,6 +1,6 @@
1
1
  lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "relaton/version"
3
+ require "relaton/render/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "relaton-render"
@@ -31,5 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency "isodoc-i18n"
32
32
  spec.add_dependency "liquid", "~> 4"
33
33
  spec.add_dependency "nokogiri"
34
+ spec.add_dependency "relaton-bib", ">= 1.11.0"
34
35
  spec.add_dependency "twitter_cldr"
36
+ spec.add_dependency "tzinfo-data" # we need this for windows only
37
+ #spec.metadata["rubygems_mfa_required"] = "true"
35
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-render
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-01 00:00:00.000000000 Z
11
+ date: 2022-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: relaton-bib
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: 1.11.0
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 1.11.0
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: twitter_cldr
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: tzinfo-data
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
153
181
  description: Rendering of ISO 690 XML
154
182
  email:
155
183
  - open.source@ribose.com
@@ -175,17 +203,19 @@ files:
175
203
  - lib/isodoc-yaml/i18n-ru.yaml
176
204
  - lib/isodoc-yaml/i18n-zh-Hans.yaml
177
205
  - lib/isodoc/i18n.rb
178
- - lib/parse/parse.rb
179
- - lib/parse/parse_contributors.rb
180
- - lib/parse/parse_extract.rb
181
206
  - lib/relaton-render.rb
182
- - lib/relaton/config.yml
183
- - lib/relaton/render.rb
184
- - lib/relaton/render_classes.rb
185
- - lib/relaton/render_fields.rb
186
- - lib/relaton/version.rb
187
- - lib/template/template.rb
188
- - lib/utils/utils.rb
207
+ - lib/relaton/render/fields/date.rb
208
+ - lib/relaton/render/fields/fields.rb
209
+ - lib/relaton/render/general/config.yml
210
+ - lib/relaton/render/general/render.rb
211
+ - lib/relaton/render/general/render_classes.rb
212
+ - lib/relaton/render/parse/parse.rb
213
+ - lib/relaton/render/parse/parse_contributors.rb
214
+ - lib/relaton/render/parse/parse_extract.rb
215
+ - lib/relaton/render/template/liquid.rb
216
+ - lib/relaton/render/template/template.rb
217
+ - lib/relaton/render/utils/utils.rb
218
+ - lib/relaton/render/version.rb
189
219
  - relaton-render.gemspec
190
220
  homepage: https://github.com/relaton/relaton-render
191
221
  licenses:
data/lib/parse/parse.rb DELETED
@@ -1,54 +0,0 @@
1
- require "nokogiri"
2
- require "twitter_cldr"
3
- require_relative "parse_contributors"
4
- require_relative "parse_extract"
5
-
6
- class Iso690Parse
7
- def initialize; end
8
-
9
- def extract(doc)
10
- host = doc.at("./relation[@type = 'includedIn']/bibitem")
11
- simple_xml2hash(doc).merge(simple_or_host_xml2hash(doc, host))
12
- .merge(host_xml2hash(host))
13
- .merge(series_xml2hash(doc, host))
14
- end
15
-
16
- def simple_xml2hash(doc)
17
- creators, role = creatornames(doc)
18
- { type: type(doc), title: title(doc), extent_raw: extent(doc),
19
- standardidentifier: standardidentifier(doc), uri: uri(doc),
20
- status: status(doc), creators: creators, role_raw: role }
21
- end
22
-
23
- def simple_or_host_xml2hash(doc, host)
24
- { edition_raw: edition(doc, host), medium: medium(doc, host),
25
- place: place(doc, host), publisher: publisher(doc, host),
26
- distributor: distributor(doc, host),
27
- access_location: access_location(doc, host),
28
- date: date(doc, host), date_updated: date_updated(doc, host),
29
- date_accessed: date_accessed(doc, host) }
30
- end
31
-
32
- def host_xml2hash(host)
33
- creators, role = creatornames(host)
34
- { host_creators: creators, host_role_raw: role, host_title: title(host) }
35
- end
36
-
37
- def series_xml2hash(doc, host)
38
- series = doc.at("./series[@type = 'main']") ||
39
- doc.at("./series[not(@type)]") || doc.at("./series")
40
- host and series ||=
41
- host.at("./series[@type = 'main']") ||
42
- host.at("./series[not(@type)]") || host.at("./series")
43
-
44
- series_xml2hash1(series)
45
- end
46
-
47
- def series_xml2hash1(series)
48
- return {} unless series
49
-
50
- { series_title: series_title(series), series_abbr: series_abbr(series),
51
- series_run: series_run(series), series_num: series_num(series),
52
- series_partnumber: series_partnumber(series) }
53
- end
54
- end
@@ -1,96 +0,0 @@
1
- class Iso690Parse
2
- def extract_orgname(org)
3
- name = org.at("./name")
4
- name&.text
5
- end
6
-
7
- def extract_personname(person)
8
- surname = person.at("./name/surname") || person.at("./name/completename")
9
- given, middle, initials = given_and_middle_name(person)
10
- { surname: surname&.text,
11
- given: given,
12
- middle: middle,
13
- initials: initials }
14
- end
15
-
16
- def given_and_middle_name(person)
17
- forenames = person.xpath("./name/forename")&.map(&:text)
18
- initials = person.xpath("./name/initial")&.map(&:text)
19
- forenames.empty? and initials.empty? and return [nil, nil, nil]
20
- forenames.empty? and forenames = initials.dup
21
- initials.empty? and initials = forenames.map { |x| x[0] }
22
- [forenames.first, forenames[1..-1], initials]
23
- end
24
-
25
- def extractname(contributor)
26
- org = contributor.at("./organization")
27
- person = contributor.at("./person")
28
- return { surname: extract_orgname(org) } if org
29
- return extract_personname(person) if person
30
-
31
- nil
32
- end
33
-
34
- def contributor_role(contributors)
35
- return nil unless contributors.length.positive?
36
-
37
- desc = contributors[0].at("role/description")&.text
38
- type = contributors[0].at("role/@type")&.text
39
- return nil if %w(author publisher).include?(type) && desc.nil?
40
-
41
- type
42
- end
43
-
44
- def creatornames(doc)
45
- cr = creatornames1(doc)
46
- cr.empty? and return [nil, nil]
47
- [cr.map { |x| extractname(x) }, contributor_role(cr)]
48
- end
49
-
50
- def creatornames1(doc)
51
- cr = []
52
- return cr if doc.nil?
53
-
54
- %w(author performer adapter translator editor publisher distributor)
55
- .each do |r|
56
- add = doc.xpath("./contributor[role/@type = '#{r}']")
57
- next if add.empty?
58
-
59
- cr = add and break
60
- end
61
- cr.empty? and cr = doc.xpath("./contributor")
62
- cr
63
- end
64
-
65
- def date1(date)
66
- on = date.at("./on")
67
- from = date.at("./from")
68
- to = date.at("./to")
69
- return { on: on.text } if on
70
- return { from: from.text, to: to&.text } if from
71
-
72
- nil
73
- end
74
-
75
- def date(doc, host)
76
- x = doc.at("./date[@type = 'issued']") ||
77
- doc.at("./date[@type = 'circulated']") ||
78
- doc.at("./date") ||
79
- host&.at("./date[@type = 'issued']") ||
80
- host&.at("./date[@type = 'circulated']") ||
81
- host&.at("./date") or return nil
82
- date1(x)
83
- end
84
-
85
- def date_updated(doc, host)
86
- x = doc.at("./date[@type = 'updated']") ||
87
- host&.at("./date[@type = 'updated']") or return nil
88
- date1(x)
89
- end
90
-
91
- def date_accessed(doc, host)
92
- x = doc.at("./date[@type = 'accessed']") ||
93
- host&.at("./date[@type = 'accessed']") or return nil
94
- date1(x)
95
- end
96
- end