relaton-render 3.0.0.pre.alpha.3 → 3.0.0.pre.alpha.5

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/render/general.rb +5 -1
  3. data/lib/relaton/render/iso690/element.rb +60 -1
  4. data/lib/relaton/render/iso690/elements/access.rb +44 -0
  5. data/lib/relaton/render/iso690/elements/component_part.rb +61 -14
  6. data/lib/relaton/render/iso690/elements/creator.rb +40 -30
  7. data/lib/relaton/render/iso690/elements/edition.rb +18 -2
  8. data/lib/relaton/render/iso690/elements/extent.rb +51 -0
  9. data/lib/relaton/render/iso690/elements/location.rb +17 -2
  10. data/lib/relaton/render/iso690/elements/medium.rb +7 -2
  11. data/lib/relaton/render/iso690/elements/production.rb +10 -2
  12. data/lib/relaton/render/iso690/elements/pub_date.rb +2 -1
  13. data/lib/relaton/render/iso690/elements/series.rb +23 -1
  14. data/lib/relaton/render/iso690/elements/size.rb +37 -0
  15. data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
  16. data/lib/relaton/render/iso690/elements/title.rb +23 -4
  17. data/lib/relaton/render/iso690/elements.rb +8 -0
  18. data/lib/relaton/render/iso690/fields.rb +14 -2
  19. data/lib/relaton/render/iso690/i18n/en.yml +22 -0
  20. data/lib/relaton/render/iso690/identifier_kinds.rb +6 -2
  21. data/lib/relaton/render/iso690/kinds.rb +6 -2
  22. data/lib/relaton/render/iso690/renderer.rb +11 -4
  23. data/lib/relaton/render/iso690/style/locale.rb +6 -1
  24. data/lib/relaton/render/iso690/style/name_form.rb +10 -0
  25. data/lib/relaton/render/iso690/style/scheme.rb +2 -0
  26. data/lib/relaton/render/iso690/style/template_map.rb +4 -0
  27. data/lib/relaton/render/iso690/style/type_template.rb +8 -0
  28. data/lib/relaton/render/iso690/style.rb +23 -4
  29. data/lib/relaton/render/version.rb +1 -1
  30. data/lib/relaton-render.rb +4 -0
  31. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1d41488064d32008b3f44f81635f4bf8e096341adb5434eb270b440b9f7bfbe
4
- data.tar.gz: 7b3b00fdadc97f66749d98f028684016a7d917977a3900c2d852a6127e4126f2
3
+ metadata.gz: 8cf356647bbf5280c9c9e4300c861917b428fc8d248db8a94b3c7b66916a0c01
4
+ data.tar.gz: 81bbe019e929ae84c4db8ca0808a10029c55dce0f61c94e072aa03c9d01127be
5
5
  SHA512:
6
- metadata.gz: 1c73e86c124a5cb571305da9b9dc8ac0a35eeabadc89f2d9fa819ca95edd3bbe097356c4e5e4f2199542c94a0939338bfd3b04d6f1f3933708b266751b5b75c9
7
- data.tar.gz: 539376d05813f377ca9d8fa36dff0c023e477daf4c7fb435b487a4e2da22bb98e185f65a1ff3a2e17b838e881028a8f7a3c7de1a27f7c5d7f5f616274f71c349
6
+ metadata.gz: 61831187b3aba40f2e3701e00b0af1d89e25f219dc735cbb1d36877a0431c0e548776b39d1b90ee8606dfdb4968962a58f61c82f83ba734b9ba4756bb02cd11e
7
+ data.tar.gz: 640b3659f2bd48a9b6cfb46c063f8d4ed7ecfe15fef2c7961c208875b56a9c7514a0641edcad9e3f335ba7fab91581044d832b737944c9a8c7a9cd134d77cb23
@@ -56,7 +56,11 @@ module Relaton
56
56
  def render_all(bib, type: "author-date")
57
57
  items = facade_bibitems(bib) or return nil
58
58
  items.each_with_object({}).with_index do |(item, m), i|
59
- ref = @renderer.render(item)
59
+ ref = begin
60
+ @renderer.render(item)
61
+ rescue ::Relaton::Render::Unrenderable
62
+ next
63
+ end
60
64
  m[item.id] = {
61
65
  id: item.id, ord: i, formattedref: ref,
62
66
  citation: {
@@ -8,10 +8,11 @@ module Relaton
8
8
  # predicate, #render the element's formatting method. Punctuation
9
9
  # between elements belongs to the kind/style, not the element.
10
10
  class Element
11
- def initialize(model, style:, i18n:)
11
+ def initialize(model, style:, i18n:, kind: nil)
12
12
  @model = model
13
13
  @style = style
14
14
  @i18n = i18n
15
+ @kind = kind
15
16
  end
16
17
 
17
18
  def present?
@@ -28,6 +29,14 @@ module Relaton
28
29
  value&.content.to_s
29
30
  end
30
31
 
32
+ # Localizable values scoped to the render language: the variants
33
+ # declared in that language, or all of them when none matches
34
+ def titles_in_lang(values)
35
+ titles = Array(values)
36
+ in_lang = titles.select { |t| Array(t.language).include?(@i18n.lang) }
37
+ in_lang.empty? ? titles : in_lang
38
+ end
39
+
31
40
  def contributors(role)
32
41
  Array(@model.contributor).select { |c| has_role?(c, role) }
33
42
  end
@@ -37,6 +46,56 @@ module Relaton
37
46
  r.is_a?(String) ? r == role : r.type == role
38
47
  end
39
48
  end
49
+
50
+ def org_name(contributor)
51
+ name = Array(contributor.organization&.name)
52
+ .map { |n| localized(n).upcase }.join(", ")
53
+ abbrev = localized(contributor.organization&.abbreviation)
54
+ return name if name.empty? || abbrev.empty?
55
+
56
+ "#{name} (#{abbrev})"
57
+ end
58
+
59
+ def item_kind
60
+ @kind || Kinds.kind_for(@model.type)
61
+ end
62
+
63
+ def person_completename(person)
64
+ localized(person.name&.completename)
65
+ end
66
+
67
+ def person_surname(person)
68
+ localized(person.name&.surname)
69
+ end
70
+
71
+ def person_given(person)
72
+ parts = Array(person.name&.forename).map do |f|
73
+ content = localized(f)
74
+ next content unless content.empty?
75
+ next "#{f.initial}." if f.initial.to_s.length == 1
76
+
77
+ ""
78
+ end.reject(&:empty?)
79
+ return localized(person.name&.formatted_initials) if parts.empty?
80
+ return parts.map { |p| "#{p[0]}." }.join(" ") if initials?
81
+
82
+ parts.join(" ")
83
+ end
84
+
85
+ def initials?
86
+ @style.scheme.name_form.initials
87
+ end
88
+
89
+ def join_names(names)
90
+ case names.size
91
+ when 0 then ""
92
+ when 1 then names.first
93
+ when 2 then names.join(" #{@i18n.label('and')} ")
94
+ else
95
+ "#{names[0..-2].join(', ')}#{@i18n.label('oxford_comma')} " \
96
+ "#{@i18n.label('and')} #{names.last}"
97
+ end
98
+ end
40
99
  end
41
100
  end
42
101
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Relaton
6
+ module Render
7
+ module Iso690
8
+ module Elements
9
+ # Date of access (ISO 690 clause 7.12): "[viewed: September 3,
10
+ # 2019]" for online resources consulted.
11
+ class Access < Element
12
+ def present?
13
+ !date_text.empty?
14
+ end
15
+
16
+ def render
17
+ "[#{@i18n.label('viewed')}: #{date_text}]"
18
+ end
19
+
20
+ private
21
+
22
+ def date_text
23
+ d = Array(@model.date).find { |x| x.type == "accessed" } or return ""
24
+ value = (d.at || d.from || d.to).to_s
25
+ parts = value.split("-")
26
+ case parts.size
27
+ when 3
28
+ date = ::Date.parse(value)
29
+ "#{month(date.month)} #{date.day}, #{date.year}"
30
+ when 2
31
+ "#{month(parts[1].to_i)} #{parts[0]}"
32
+ else
33
+ value
34
+ end
35
+ end
36
+
37
+ def month(num)
38
+ @i18n.label("month_#{num}")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -4,44 +4,91 @@ module Relaton
4
4
  module Render
5
5
  module Iso690
6
6
  module Elements
7
- # Component part within a host (ISO 690 clause 7.4): "In: host
8
- # title, edition of host". Host data comes from the typed
9
- # partOf relation.
7
+ # Component part within a host (ISO 690 clause 7.4): host title
8
+ # (emphasised), host editors, host edition, host place and
9
+ # publisher. Host data comes from the typed partOf/includedIn
10
+ # relation.
10
11
  class ComponentPart < Element
12
+ HOST_RELATION_TYPES = %w[partOf includedIn].freeze
13
+
11
14
  def present?
12
- !host_title.empty?
15
+ !host_title_text.empty?
13
16
  end
14
17
 
15
18
  def render
16
- "#{@i18n.label('in')} " \
17
- "#{@style.templates.title_open}#{host_title}" \
18
- "#{@style.templates.title_close}" \
19
- "#{host_edition}"
19
+ "#{lead}#{host_title}#{host_editors}#{host_edition}" \
20
+ "#{host_production}"
20
21
  end
21
22
 
22
23
  private
23
24
 
25
+ def lead
26
+ label = @i18n.label("in")
27
+ label.empty? ? "" : "#{label} "
28
+ end
29
+
24
30
  def relation
25
- Array(@model.relation).find { |r| r.type == "partOf" }
31
+ Array(@model.relation).find do |r|
32
+ HOST_RELATION_TYPES.include?(r.type)
33
+ end
26
34
  end
27
35
 
28
36
  def host
29
37
  relation&.bibitem
30
38
  end
31
39
 
32
- def host_title
40
+ def host_title_text
33
41
  return localized(relation&.description) if host.nil?
34
42
 
35
- Array(host.title).map { |t| localized(t) }.join(" ").strip
43
+ titles_in_lang(host.title).map { |t| localized(t) }
44
+ .join(" ").strip
45
+ end
46
+
47
+ def host_title
48
+ return "" if host_title_text.empty?
49
+
50
+ "#{@style.templates.title_open}#{host_title_text}" \
51
+ "#{@style.templates.title_close}"
52
+ end
53
+
54
+ def host_editors
55
+ eds = Array(host&.contributor).select { |c| has_role?(c, "editor") }
56
+ names = eds.each_with_index
57
+ .map { |c, i| host_person_name(c, first: i.zero?) }
58
+ .reject(&:empty?)
59
+ return "" if names.empty?
60
+
61
+ " (#{@i18n.label(eds.one? ? 'ed' : 'eds')} #{join_names(names)})"
62
+ end
63
+
64
+ # ISO 690 clause 7.2 host editors: the first name inverted
65
+ # (surname, initials), subsequent names in direct order, all in
66
+ # mixed case
67
+ def host_person_name(contributor, first:)
68
+ person = contributor.person or return ""
69
+
70
+ surname = person_surname(person)
71
+ given = person_given(person)
72
+ if first
73
+ [surname, given].reject(&:empty?).join(", ")
74
+ else
75
+ [given, surname].reject(&:empty?).join(" ")
76
+ end
36
77
  end
37
78
 
38
79
  def host_edition
39
80
  return "" if host.nil?
40
81
 
41
- edition = Edition.new(host, style: @style, i18n: @i18n)
42
- return "" unless edition.present?
82
+ edition = Edition.new(host, style: @style, i18n: @i18n,
83
+ kind: item_kind)
84
+ edition.present? ? ". #{edition.render}" : ""
85
+ end
86
+
87
+ def host_production
88
+ return "" if host.nil?
43
89
 
44
- ". #{edition.render}"
90
+ production = Production.new(host, style: @style, i18n: @i18n)
91
+ production.present? ? ". #{production.render}" : ""
45
92
  end
46
93
  end
47
94
  end
@@ -15,7 +15,32 @@ module Relaton
15
15
  end
16
16
 
17
17
  def render
18
- "#{join(names)}#{role_suffix}"
18
+ case role_placement
19
+ when "afterPeriod"
20
+ # the in-slot trailing space separates the (eds.) marker
21
+ # from the next element, which follows directly in the
22
+ # template; a lone editor is unmarked
23
+ if editors? && creators.size > 1
24
+ "#{period(join(names))} (#{role_word}) "
25
+ else
26
+ "#{period(join(names))} "
27
+ end
28
+ else
29
+ marker = editors? && creators.size > 1 ? " (#{role_word})" : ''
30
+ "#{join(names)}#{marker}"
31
+ end
32
+ end
33
+
34
+ def role_placement
35
+ @style.scheme.name_form.role_placement || "glue"
36
+ end
37
+
38
+ def period(text)
39
+ text.end_with?(".") ? text : "#{text}."
40
+ end
41
+
42
+ def role_word
43
+ @i18n.label(creators.one? ? "ed" : "eds")
19
44
  end
20
45
 
21
46
  # In-text (name-and-date) form: the principal creator only
@@ -55,11 +80,19 @@ module Relaton
55
80
 
56
81
  def format(contributor, first:)
57
82
  person = contributor.person or return org_name(contributor)
58
- name = completename(person) ||
83
+ name = completename_name(person, first: first) ||
59
84
  personal_name(person, first: first)
60
85
  name.empty? ? org_name(contributor) : name
61
86
  end
62
87
 
88
+ def completename_name(person, first:)
89
+ complete = completename(person) or return nil
90
+ return complete unless name_form.completename_upcase
91
+
92
+ first ? @style.render_name(surname: complete, given: "") :
93
+ complete.upcase
94
+ end
95
+
63
96
  def personal_name(person, first:)
64
97
  surname = surname(person).to_s
65
98
  given = given_names(person)
@@ -75,21 +108,7 @@ module Relaton
75
108
  end
76
109
 
77
110
  def join(names)
78
- case names.size
79
- when 0 then ""
80
- when 1 then names.first
81
- when 2 then names.join(" #{@i18n.label('and')} ")
82
- else
83
- "#{names[0..-2].join(', ')}#{@i18n.label('oxford_comma')} " \
84
- "#{@i18n.label('and')} #{names.last}"
85
- end
86
- end
87
-
88
- def role_suffix
89
- return "" unless editors?
90
-
91
- suffix = creators.one? ? @i18n.label("ed") : @i18n.label("eds")
92
- " (#{suffix})"
111
+ join_names(names)
93
112
  end
94
113
 
95
114
  def editors?
@@ -97,30 +116,21 @@ module Relaton
97
116
  end
98
117
 
99
118
  def completename(person)
100
- value = localized(person.name&.completename)
119
+ value = person_completename(person)
101
120
  value.empty? ? nil : value
102
121
  end
103
122
 
104
123
  def surname(person)
105
- value = localized(person.name&.surname)
124
+ value = person_surname(person)
106
125
  value.empty? ? nil : value
107
126
  end
108
127
 
109
128
  def given_names(person)
110
- names = Array(person.name&.forename).map { |f| localized(f) }
111
- .reject(&:empty?)
112
- return names.map { |n| "#{n[0]}." }.join(" ") if name_form.initials
113
-
114
- names.join(" ")
129
+ person_given(person)
115
130
  end
116
131
 
117
132
  def org_name(contributor)
118
- name = Array(contributor.organization&.name)
119
- .map { |n| localized(n).upcase }.join(", ")
120
- abbrev = localized(contributor.organization&.abbreviation)
121
- return name if name.empty? || abbrev.empty?
122
-
123
- "#{name} (#{abbrev})"
133
+ super
124
134
  end
125
135
  end
126
136
  end
@@ -8,17 +8,33 @@ module Relaton
8
8
  # edition through Edition#number (falling back to its content).
9
9
  class Edition < Element
10
10
  def present?
11
- !number.zero?
11
+ !raw.to_s.empty?
12
12
  end
13
13
 
14
14
  def render
15
+ return "#{@i18n.label('version')} #{raw}" unless word_edition?
16
+
17
+ word = @i18n.label("edition_#{number}")
18
+ return word unless word == "edition_#{number}"
19
+
15
20
  "#{ordinalize(number)} #{@i18n.label('edition')}"
16
21
  end
17
22
 
18
23
  private
19
24
 
25
+ # Worded editions are document editions; online resources
26
+ # carry numbered builds rendered as versions
27
+ def word_edition?
28
+ number.positive? &&
29
+ %w[monograph component_part serial_part]
30
+ .include?(item_kind)
31
+ end
32
+
33
+ def raw
34
+ (@model.edition&.number || @model.edition&.content).to_s
35
+ end
36
+
20
37
  def number
21
- raw = @model.edition&.number || @model.edition&.content
22
38
  Integer(raw)
23
39
  rescue ArgumentError, TypeError
24
40
  0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
7
+ # Extent of the item (ISO 690 clause 7.11): page localities.
8
+ class Extent < Element
9
+ def present?
10
+ !render.empty?
11
+ end
12
+
13
+ def render
14
+ groups = [numeration, page_range].reject { |g| g.empty? }
15
+ groups.join(", ")
16
+ end
17
+
18
+ private
19
+
20
+ def numeration
21
+ %w[volume issue].filter_map do |type|
22
+ loc = localities.find { |l| l.type == type }
23
+ next if loc.nil? || loc.reference_from.to_s.empty?
24
+
25
+ "#{@i18n.label(type)} #{loc.reference_from}"
26
+ end.join(" ")
27
+ end
28
+
29
+ def page_range
30
+ loc = localities.find { |l| l.type == "page" } or return ""
31
+ from = loc.reference_from.to_s
32
+ to = loc.reference_to.to_s
33
+ if to.empty? || to == from
34
+ "#{@i18n.label('page')} #{from}"
35
+ else
36
+ "#{@i18n.label('pages')} " \
37
+ "#{from}#{@i18n.label('date_range')}#{to}"
38
+ end
39
+ end
40
+
41
+ def localities
42
+ Array(@model.extent).flat_map do |e|
43
+ Array(e.locality) +
44
+ Array(e.locality_stack).flat_map { |s| Array(s.locality) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -12,13 +12,28 @@ module Relaton
12
12
  end
13
13
 
14
14
  def render
15
- "#{@i18n.label('available_from')} #{uri}"
15
+ text = if (form = @style.templates.uri) && !form.empty?
16
+ ::Relaton::Render::Iso690::Template.new(form).evaluate(
17
+ "uri" => Template::Field[!uri.empty?, uri],
18
+ )
19
+ else
20
+ uri
21
+ end
22
+
23
+ "#{@i18n.label('available_from')} #{text}".strip
16
24
  end
17
25
 
18
26
  private
19
27
 
20
28
  def uri
21
- Array(@model.accesslocation).reject(&:empty?).first.to_s
29
+ from_accesslocation =
30
+ Array(@model.accesslocation).map(&:to_s).reject(&:empty?)
31
+ unless from_accesslocation.empty?
32
+ return from_accesslocation.first.to_s
33
+ end
34
+
35
+ Array(@model.source).map { |u| u.content.to_s }
36
+ .reject(&:empty?).first.to_s
22
37
  end
23
38
  end
24
39
  end
@@ -12,13 +12,18 @@ module Relaton
12
12
  end
13
13
 
14
14
  def render
15
- "[#{medium}]"
15
+ medium
16
16
  end
17
17
 
18
18
  private
19
19
 
20
20
  def medium
21
- localized(@model.medium)
21
+ genre = @model.medium&.genre.to_s
22
+ return genre.sub(/^\w/) { |c| c.upcase } unless genre.empty?
23
+
24
+ key = "medium_#{@model.type}"
25
+ label = @i18n.label(key)
26
+ label == key ? "" : label
22
27
  end
23
28
  end
24
29
  end
@@ -20,8 +20,16 @@ module Relaton
20
20
  private
21
21
 
22
22
  def places
23
- Array(@model.place).map { |p| p.city.to_s }
24
- .reject { |c| c.strip.empty? }
23
+ Array(@model.place).map do |p|
24
+ structured = [p.city.to_s, p.region.map(&:content),
25
+ p.country.map(&:content)]
26
+ .flatten.reject { |c| c.strip.empty? }
27
+ if structured.empty? && !p.formatted_place.to_s.empty?
28
+ next p.formatted_place.to_s
29
+ end
30
+
31
+ structured.join(", ")
32
+ end.reject(&:empty?)
25
33
  end
26
34
 
27
35
  def publishers
@@ -21,7 +21,8 @@ module Relaton
21
21
 
22
22
  def publication_date
23
23
  Array(@model.date).find(&:published) ||
24
- Array(@model.date).find { |d| d.type.nil? }
24
+ Array(@model.date).find { |d| d.type.nil? } ||
25
+ Array(@model.date).find { |d| d.type == "created" }
25
26
  end
26
27
 
27
28
  def publication_date_value
@@ -11,6 +11,15 @@ module Relaton
11
11
  end
12
12
 
13
13
  def render
14
+ if (form = series_form) && !form.empty?
15
+ return ::Relaton::Render::Iso690::Template.new(form).evaluate(
16
+ "seriestitle" => Template::Field[!series_title.empty?,
17
+ series_title],
18
+ "seriesnumber" => Template::Field[!number.empty?, number],
19
+ "seriesrun" => Template::Field[!run.empty?, run],
20
+ )
21
+ end
22
+
14
23
  base = "#{@style.templates.title_open}#{series_title}" \
15
24
  "#{@style.templates.title_close}"
16
25
  if number.empty?
@@ -27,12 +36,25 @@ module Relaton
27
36
  end
28
37
 
29
38
  def series_title
30
- Array(series&.title).map { |t| localized(t) }.join(" ").strip
39
+ titles_in_lang(series&.title).map { |t| localized(t) }
40
+ .join(" ").strip
31
41
  end
32
42
 
33
43
  def number
34
44
  series&.number.to_s
35
45
  end
46
+
47
+ def run
48
+ series&.run.to_s
49
+ end
50
+
51
+ # The style's series form for this item's resource kind, falling
52
+ # back to the style-wide form
53
+ def series_form
54
+ variant = @style.per_type
55
+ .find { |t| t.type == Kinds.kind_for(@model.type) }
56
+ variant&.series || @style.templates.series
57
+ end
36
58
  end
37
59
  end
38
60
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
7
+ # Physical size (ISO 690 clause 7.5, area): the model's typed
8
+ # values render as "N unit" through the language pack; untyped
9
+ # values pass through as written.
10
+ class Size < Element
11
+ def present?
12
+ !values.empty?
13
+ end
14
+
15
+ def render
16
+ values.map { |v| render_value(v) }.join(", ")
17
+ end
18
+
19
+ private
20
+
21
+ def values
22
+ Array(@model.size&.value)
23
+ end
24
+
25
+ def render_value(value)
26
+ content = value.is_a?(String) ? value : value.content.to_s
27
+ return content if content.empty? || value.is_a?(String)
28
+
29
+ type = value.is_a?(Hash) ? value[:type] : value.type
30
+ unit = type && @i18n.label("size_#{type}")
31
+ unit.nil? || unit == "size_#{type}" ? content : "#{content} #{unit}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
7
+ # Issuing body or status of a standard (ISO 690 clause 8.9): the
8
+ # publisher or authorizer organization, falling back to the
9
+ # capitalized status stage.
10
+ class Stddoc < Element
11
+ def present?
12
+ !body.empty?
13
+ end
14
+
15
+ def render
16
+ body
17
+ end
18
+
19
+ private
20
+
21
+ def body
22
+ orgs = %w[publisher authorizer].flat_map { |role|
23
+ contributors(role).map { |c| org_plain(c) }
24
+ }.reject(&:empty?)
25
+ return orgs.join("; ") unless orgs.empty?
26
+
27
+ stage = @model.status&.stage&.content.to_s
28
+ stage.empty? ? "" : stage.sub(/^\w/) { |c| c.upcase }
29
+ end
30
+
31
+ # Issuing bodies render in mixed case, unlike creator organs
32
+ def org_plain(contributor)
33
+ Array(contributor.organization&.name)
34
+ .map { |n| localized(n) }.join(", ")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -12,16 +12,35 @@ module Relaton
12
12
  end
13
13
 
14
14
  def render
15
- "#{@style.templates.title_open}#{main_title}" \
15
+ text = main_title
16
+ return text if analytic?
17
+ if (form = @style.title_form_for(Kinds.kind_for(@model.type)))
18
+ text = ::Relaton::Render::Iso690::Template.new(form).evaluate(
19
+ "title" => Template::Field[!text.empty?, text],
20
+ )
21
+ end
22
+
23
+ "#{@style.templates.title_open}#{text}" \
16
24
  "#{@style.templates.title_close}"
17
25
  end
18
26
 
19
27
  private
20
28
 
29
+ # ISO 690: analytic titles are not emphasised; the host or
30
+ # serial carries the emphasis
31
+ def analytic?
32
+ %w[component_part serial_part]
33
+ .include?(Kinds.kind_for(@model.type))
34
+ end
35
+
36
+ # A typed main title wins; an untyped title is main by
37
+ # default, but yields to a typed one
21
38
  def main_title
22
- t = Array(@model.title).find { |x| (x.type || "main") == "main" } ||
23
- Array(@model.title).first
24
- localized(t)
39
+ titles = Array(@model.title)
40
+ mains = titles.select { |x| x.type == "main" }
41
+ mains = titles.select { |x| x.type.nil? } if mains.empty?
42
+ mains = titles if mains.empty?
43
+ localized(titles_in_lang(mains).first)
25
44
  end
26
45
  end
27
46
  end
@@ -16,6 +16,10 @@ module Relaton
16
16
  autoload :Location, "relaton/render/iso690/elements/location"
17
17
  autoload :Numeration, "relaton/render/iso690/elements/numeration"
18
18
  autoload :ComponentPart, "relaton/render/iso690/elements/component_part"
19
+ autoload :Size, "relaton/render/iso690/elements/size"
20
+ autoload :Extent, "relaton/render/iso690/elements/extent"
21
+ autoload :Access, "relaton/render/iso690/elements/access"
22
+ autoload :Stddoc, "relaton/render/iso690/elements/stddoc"
19
23
 
20
24
  CLASSES = {
21
25
  creator: Creator,
@@ -29,6 +33,10 @@ module Relaton
29
33
  location: Location,
30
34
  numeration: Numeration,
31
35
  component_part: ComponentPart,
36
+ size: Size,
37
+ extent: Extent,
38
+ access: Access,
39
+ stddoc: Stddoc,
32
40
  }.freeze
33
41
 
34
42
  def self.build(name, model, style:, i18n:)
@@ -10,7 +10,8 @@ module Relaton
10
10
  class Fields
11
11
  ELEMENT_SLOTS = %i[
12
12
  creator title edition medium series production
13
- date numeration component_part identifier location
13
+ date numeration component_part identifier location size
14
+ extent access stddoc
14
15
  ].freeze
15
16
 
16
17
  def initialize(model, style:, i18n:, disambiguator: nil)
@@ -34,7 +35,18 @@ module Relaton
34
35
  def element_field(slot)
35
36
  element = Elements.build(slot, @model, style: @style, i18n: @i18n)
36
37
  text = element.render.to_s
37
- Template::Field[element.present? && !text.empty?, text]
38
+ if element.present? && !text.empty?
39
+ return Template::Field[true, text]
40
+ end
41
+
42
+ fallback = @style.fallback_for(item_kind, slot)
43
+ return Template::Field[true, fallback] if fallback
44
+
45
+ Template::Field[false, ""]
46
+ end
47
+
48
+ def item_kind
49
+ Kinds.kind_for(@model.type)
38
50
  end
39
51
 
40
52
  def name_fields
@@ -1,6 +1,28 @@
1
1
  lang: en
2
2
  labels:
3
3
  edition: "ed"
4
+ size_volume: "vol."
5
+ size_page: "pp."
6
+ page: "p."
7
+ pages: "pp."
8
+ volume: "vol."
9
+ issue: "no."
10
+ version: "Version"
11
+ medium_website: "[website]"
12
+ viewed: "viewed"
13
+ month_1: "January"
14
+ month_2: "February"
15
+ month_3: "March"
16
+ month_4: "April"
17
+ month_5: "May"
18
+ month_6: "June"
19
+ month_7: "July"
20
+ month_8: "August"
21
+ month_9: "September"
22
+ month_10: "October"
23
+ month_11: "November"
24
+ month_12: "December"
25
+
4
26
  series_no: "no."
5
27
  report_no: "Report no."
6
28
  available_from: "Available from:"
@@ -16,15 +16,19 @@ module Relaton
16
16
  "MRN" => ->(content) { content },
17
17
  }.freeze
18
18
 
19
+ # Flavor-registered kinds, consulted before the built-ins; the
20
+ # registry is process-wide by design
21
+ OVERRIDES = {}
22
+
19
23
  class << self
20
24
  def register(type, kind)
21
- KINDS[type] = kind
25
+ OVERRIDES[type] = kind
22
26
  kind
23
27
  end
24
28
 
25
29
  def render(docidentifier)
26
30
  content = docidentifier.content.to_s
27
- kind = KINDS[docidentifier.type]
31
+ kind = OVERRIDES[docidentifier.type] || KINDS[docidentifier.type]
28
32
  kind ? kind.call(content) : content
29
33
  end
30
34
  end
@@ -26,8 +26,12 @@ module Relaton
26
26
  "inbook" => "component_part",
27
27
  "incollection" => "component_part",
28
28
  "inproceedings" => "component_part",
29
- "website" => "online",
30
- "webresource" => "online",
29
+ "article" => "serial_part",
30
+ "software" => "online",
31
+ "unpublished" => "online",
32
+ "dataset" => "online",
33
+ "website" => "webdoc",
34
+ "webresource" => "webdoc",
31
35
  "webpage" => "online",
32
36
  "online" => "online",
33
37
  }.freeze
@@ -8,8 +8,6 @@ module Relaton
8
8
  # the template evaluator to the field resolver. No XML, no
9
9
  # intermediate hashes.
10
10
  class Renderer
11
- class Unrenderable < StandardError; end
12
-
13
11
  DEFAULT_STYLE = "author-date"
14
12
 
15
13
  class << self
@@ -33,14 +31,23 @@ module Relaton
33
31
  end
34
32
 
35
33
  def render(model)
36
- out = Template.new(@style.template_for(Kinds.kind_for(model.type))).evaluate(
34
+ kind = Kinds.kind_for(model.type)
35
+ template = @style.template_for(kind, home: home_docid?(model))
36
+ out = Template.new(template).evaluate(
37
37
  Fields.new(model, style: @style, i18n: @i18n).to_h,
38
38
  )
39
- raise Unrenderable, "no renderable elements" if out.strip.empty?
39
+ raise ::Relaton::Render::Unrenderable,
40
+ "no renderable elements" if out.strip.empty?
40
41
 
41
42
  out
42
43
  end
43
44
 
45
+ def home_docid?(model)
46
+ type = @style.scheme.home_docid_type
47
+ !type.nil? && !type.empty? &&
48
+ Array(model.docidentifier).any? { |d| d.type == type }
49
+ end
50
+
44
51
  # In-text form: SURNAME, year + disambiguator (name-and-date)
45
52
  def citation(model, disambiguator: nil)
46
53
  Template.new(@style.templates.citation).evaluate(
@@ -14,6 +14,9 @@ module Relaton
14
14
  attribute :in_str, :string
15
15
  attribute :at, :string
16
16
  attribute :available_at, :string
17
+ # Arbitrary label overrides (edition words, join punctuation,
18
+ # size units): merged over the language pack verbatim.
19
+ attribute :labels, :hash, default: -> { {} }
17
20
 
18
21
  key_value do
19
22
  map "and", to: :conj
@@ -23,6 +26,7 @@ module Relaton
23
26
  map "in", to: :in_str
24
27
  map "at", to: :at
25
28
  map "availableAt", to: :available_at
29
+ map "labels", to: :labels
26
30
  end
27
31
 
28
32
  LABEL_FOR = {
@@ -32,10 +36,11 @@ module Relaton
32
36
  }.freeze
33
37
 
34
38
  def label_map
35
- LABEL_FOR.filter_map do |attr, label|
39
+ base = LABEL_FOR.filter_map do |attr, label|
36
40
  value = public_send(attr)
37
41
  [label, value] unless value.nil?
38
42
  end.to_h
43
+ base.merge(labels || {})
39
44
  end
40
45
  end
41
46
  end
@@ -9,10 +9,20 @@ module Relaton
9
9
  class NameForm < Lutaml::Model::Serializable
10
10
  attribute :initials, :boolean, default: -> { false }
11
11
  attribute :given_name_first, :boolean, default: -> { false }
12
+ # Where an all-editor creator list's role marker sits relative
13
+ # to the name's terminating period: "glue" renders
14
+ # "NAME (eds.)." (the name-date default), "afterPeriod"
15
+ # renders "NAME. (eds.)" (ISO 690 clause-flavour).
16
+ attribute :role_placement, :string, default: "glue"
17
+ # A completename is rendered, not decomposed; styles that
18
+ # upcase inverted names may declare the completename treatment
19
+ attribute :completename_upcase, :boolean, default: -> { false }
12
20
 
13
21
  key_value do
14
22
  map "initials", to: :initials
15
23
  map "givenNameFirst", to: :given_name_first
24
+ map "rolePlacement", to: :role_placement
25
+ map "completenameUpcase", to: :completename_upcase
16
26
  end
17
27
  end
18
28
  end
@@ -10,11 +10,13 @@ module Relaton
10
10
  attribute :system, :string, default: "name-date"
11
11
  attribute :name_form, NameForm, default: -> { NameForm.new }
12
12
  attribute :locale, Locale, default: -> { Locale.new }
13
+ attribute :home_docid_type, :string
13
14
 
14
15
  key_value do
15
16
  map "system", to: :system
16
17
  map "nameForm", to: :name_form
17
18
  map "locale", to: :locale
19
+ map "homeDocidType", to: :home_docid_type
18
20
  end
19
21
  end
20
22
  end
@@ -13,6 +13,8 @@ module Relaton
13
13
  attribute :citation, :string, default: "{{surname}}, {{date}}"
14
14
  attribute :reference, :string
15
15
  attribute :name, :string, default: "{{surname}}, {{givenNames}}"
16
+ attribute :series, :string, default: ""
17
+ attribute :uri, :string, default: ""
16
18
 
17
19
  key_value do
18
20
  map "titleOpen", to: :title_open
@@ -20,6 +22,8 @@ module Relaton
20
22
  map "citation", to: :citation
21
23
  map "reference", to: :reference
22
24
  map "name", to: :name
25
+ map "series", to: :series
26
+ map "uri", to: :uri
23
27
  end
24
28
  end
25
29
  end
@@ -9,10 +9,18 @@ module Relaton
9
9
  class TypeTemplate < Lutaml::Model::Serializable
10
10
  attribute :type, :string
11
11
  attribute :template, :string
12
+ attribute :series, :string
13
+ attribute :title, :string
14
+ attribute :home, :boolean
15
+ attribute :fallbacks, :hash, default: -> { {} }
12
16
 
13
17
  key_value do
14
18
  map "type", to: :type
15
19
  map "template", to: :template
20
+ map "series", to: :series
21
+ map "title", to: :title
22
+ map "home", to: :home
23
+ map "fallbacks", to: :fallbacks
16
24
  end
17
25
  end
18
26
  end
@@ -41,10 +41,29 @@ module Relaton
41
41
  end
42
42
 
43
43
  # Per-type template selection is a data lookup; unmatched types
44
- # fall back to the general reference template.
45
- def template_for(type)
46
- per_type.find { |t| t.type == type.to_s }&.template ||
47
- templates.reference
44
+ # fall back to the general reference template. When a kind has
45
+ # home-flagged variants, the split is resolved by whether the item
46
+ # carries the scheme's home document identifier.
47
+ def template_for(type, home: nil)
48
+ candidates = per_type.select { |t| t.type == type.to_s }
49
+ if candidates.size > 1 && !home.nil?
50
+ candidates = candidates.select { |t| (t.home || false) == home }
51
+ end
52
+ candidates.first&.template || templates.reference
53
+ end
54
+
55
+ def type_template_for(type)
56
+ per_type.find { |t| t.type == type.to_s }
57
+ end
58
+
59
+ # The absent-slot replacement text declared for a type, when any
60
+ def fallback_for(type, slot)
61
+ type_template_for(type)&.fallbacks&.[](slot.to_s)
62
+ end
63
+
64
+ def title_form_for(type)
65
+ per_type.lazy.select { |t| t.type == type.to_s }
66
+ .find { |t| t.title && !t.title.empty? }&.title
48
67
  end
49
68
 
50
69
  # First-creator name form: the style's declared name template with
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Render
3
- VERSION = "3.0.0.pre.alpha.3".freeze
3
+ VERSION = "3.0.0.pre.alpha.5".freeze
4
4
  end
5
5
  end
@@ -5,6 +5,10 @@ require "relaton/render/iso690"
5
5
 
6
6
  module Relaton
7
7
  module Render
8
+ # A bibliographic item whose style template resolves to nothing: the
9
+ # single-item API raises, the batch API skips
10
+ class Unrenderable < StandardError; end
11
+
8
12
  # Deprecated 1.x engine leaves, restored so flavor gems that still
9
13
  # subclass them load against this line. Superseded by the Iso690
10
14
  # engine; removed when the flavors have migrated to CitationStyle
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-render
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.pre.alpha.3
4
+ version: 3.0.0.pre.alpha.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -120,9 +120,11 @@ files:
120
120
  - lib/relaton/render/iso690.rb
121
121
  - lib/relaton/render/iso690/element.rb
122
122
  - lib/relaton/render/iso690/elements.rb
123
+ - lib/relaton/render/iso690/elements/access.rb
123
124
  - lib/relaton/render/iso690/elements/component_part.rb
124
125
  - lib/relaton/render/iso690/elements/creator.rb
125
126
  - lib/relaton/render/iso690/elements/edition.rb
127
+ - lib/relaton/render/iso690/elements/extent.rb
126
128
  - lib/relaton/render/iso690/elements/identifier.rb
127
129
  - lib/relaton/render/iso690/elements/location.rb
128
130
  - lib/relaton/render/iso690/elements/medium.rb
@@ -130,6 +132,8 @@ files:
130
132
  - lib/relaton/render/iso690/elements/production.rb
131
133
  - lib/relaton/render/iso690/elements/pub_date.rb
132
134
  - lib/relaton/render/iso690/elements/series.rb
135
+ - lib/relaton/render/iso690/elements/size.rb
136
+ - lib/relaton/render/iso690/elements/stddoc.rb
133
137
  - lib/relaton/render/iso690/elements/title.rb
134
138
  - lib/relaton/render/iso690/fields.rb
135
139
  - lib/relaton/render/iso690/i18n.rb