relaton 3.0.6 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/bin/relaton +20 -0
  3. data/lib/relaton/bib/model/docidentifier.rb +17 -2
  4. data/lib/relaton/bibcollection.rb +146 -0
  5. data/lib/relaton/bibdata.rb +92 -0
  6. data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
  7. data/lib/relaton/cli/base_convertor.rb +134 -0
  8. data/lib/relaton/cli/command.rb +390 -0
  9. data/lib/relaton/cli/data_fetcher.rb +20 -0
  10. data/lib/relaton/cli/frontend/dist/404.html +18 -0
  11. data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
  12. data/lib/relaton/cli/frontend/dist/style.css +3 -0
  13. data/lib/relaton/cli/frontend/index.html +14 -0
  14. data/lib/relaton/cli/frontend/package-lock.json +2945 -0
  15. data/lib/relaton/cli/frontend/package.json +29 -0
  16. data/lib/relaton/cli/frontend/public/404.html +18 -0
  17. data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
  18. data/lib/relaton/cli/frontend/src/App.vue +506 -0
  19. data/lib/relaton/cli/frontend/src/app.ts +90 -0
  20. data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
  21. data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
  22. data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
  23. data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
  24. data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
  25. data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
  26. data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
  27. data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
  28. data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
  29. data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
  30. data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
  31. data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
  32. data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
  33. data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
  34. data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
  35. data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
  36. data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
  37. data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
  38. data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
  39. data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
  40. data/lib/relaton/cli/frontend/tsconfig.json +20 -0
  41. data/lib/relaton/cli/frontend/vite.config.ts +42 -0
  42. data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
  43. data/lib/relaton/cli/frontend_assets.rb +58 -0
  44. data/lib/relaton/cli/full_text_search.rb +98 -0
  45. data/lib/relaton/cli/i18n.yaml +1 -0
  46. data/lib/relaton/cli/index_item_normalizer.rb +380 -0
  47. data/lib/relaton/cli/index_site_generator.rb +1015 -0
  48. data/lib/relaton/cli/relaton_file.rb +290 -0
  49. data/lib/relaton/cli/subcommand_collection.rb +263 -0
  50. data/lib/relaton/cli/subcommand_db.rb +81 -0
  51. data/lib/relaton/cli/templates/_document.liquid +111 -0
  52. data/lib/relaton/cli/templates/_index.liquid +61 -0
  53. data/lib/relaton/cli/templates/index/page.liquid +20 -0
  54. data/lib/relaton/cli/templates/index-style.css +1017 -0
  55. data/lib/relaton/cli/util.rb +8 -0
  56. data/lib/relaton/cli/version.rb +11 -0
  57. data/lib/relaton/cli/xml_convertor.rb +47 -0
  58. data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
  59. data/lib/relaton/cli/yaml_convertor.rb +102 -0
  60. data/lib/relaton/cli.rb +113 -0
  61. data/lib/relaton/doi/CLAUDE.md +1 -1
  62. data/lib/relaton/element_finder.rb +29 -0
  63. data/lib/relaton/gb/CLAUDE.md +1 -1
  64. data/lib/relaton/ietf/CLAUDE.md +1 -1
  65. data/lib/relaton/iho/CLAUDE.md +3 -3
  66. data/lib/relaton/iho/bibliography.rb +41 -27
  67. data/lib/relaton/iho/processor.rb +3 -2
  68. data/lib/relaton/nist/CLAUDE.md +11 -1
  69. data/lib/relaton/nist/README.adoc +7 -1
  70. data/lib/relaton/nist/hit_collection.rb +25 -1
  71. data/lib/relaton/nist/mods_parser.rb +32 -8
  72. data/lib/relaton/nist/pubs_export.rb +69 -11
  73. data/lib/relaton/ogc/CLAUDE.md +1 -1
  74. data/lib/relaton/omg/CLAUDE.md +1 -1
  75. data/lib/relaton/plateau/CLAUDE.md +1 -1
  76. data/lib/relaton/render/general.rb +319 -0
  77. data/lib/relaton/render/iso690/csl_import.rb +129 -0
  78. data/lib/relaton/render/iso690/element.rb +153 -0
  79. data/lib/relaton/render/iso690/elements/access.rb +44 -0
  80. data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
  81. data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
  82. data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
  83. data/lib/relaton/render/iso690/elements/creator.rb +267 -0
  84. data/lib/relaton/render/iso690/elements/edition.rb +97 -0
  85. data/lib/relaton/render/iso690/elements/extent.rb +71 -0
  86. data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
  87. data/lib/relaton/render/iso690/elements/location.rb +73 -0
  88. data/lib/relaton/render/iso690/elements/medium.rb +32 -0
  89. data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
  90. data/lib/relaton/render/iso690/elements/production.rb +111 -0
  91. data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
  92. data/lib/relaton/render/iso690/elements/series.rb +66 -0
  93. data/lib/relaton/render/iso690/elements/size.rb +45 -0
  94. data/lib/relaton/render/iso690/elements/status.rb +30 -0
  95. data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
  96. data/lib/relaton/render/iso690/elements/title.rb +56 -0
  97. data/lib/relaton/render/iso690/elements/updated.rb +38 -0
  98. data/lib/relaton/render/iso690/elements.rb +67 -0
  99. data/lib/relaton/render/iso690/fields.rb +142 -0
  100. data/lib/relaton/render/iso690/i18n/en.yml +48 -0
  101. data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
  102. data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
  103. data/lib/relaton/render/iso690/i18n.rb +83 -0
  104. data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
  105. data/lib/relaton/render/iso690/index_renderer.rb +57 -0
  106. data/lib/relaton/render/iso690/kinds.rb +53 -0
  107. data/lib/relaton/render/iso690/renderer.rb +150 -0
  108. data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
  109. data/lib/relaton/render/iso690/rules/jis.rb +103 -0
  110. data/lib/relaton/render/iso690/rules/nist.rb +384 -0
  111. data/lib/relaton/render/iso690/rules.rb +227 -0
  112. data/lib/relaton/render/iso690/style/locale.rb +59 -0
  113. data/lib/relaton/render/iso690/style/name_form.rb +87 -0
  114. data/lib/relaton/render/iso690/style/scheme.rb +33 -0
  115. data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
  116. data/lib/relaton/render/iso690/style/template_map.rb +40 -0
  117. data/lib/relaton/render/iso690/style/type_template.rb +45 -0
  118. data/lib/relaton/render/iso690/style.rb +227 -0
  119. data/lib/relaton/render/iso690/styles/apa.yml +45 -0
  120. data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
  121. data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
  122. data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
  123. data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
  124. data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
  125. data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
  126. data/lib/relaton/render/iso690/styles/mla.yml +56 -0
  127. data/lib/relaton/render/iso690/template.rb +248 -0
  128. data/lib/relaton/render/iso690.rb +28 -0
  129. data/lib/relaton/render/version.rb +5 -0
  130. data/lib/relaton/un/CLAUDE.md +1 -1
  131. data/lib/relaton/version.rb +1 -1
  132. data/lib/relaton-cli.rb +17 -0
  133. data/lib/relaton-render.rb +13 -0
  134. metadata +192 -21
@@ -0,0 +1,129 @@
1
+ require "json"
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # Imports a CSL style's common properties into a CitationStyle instance
7
+ # (styles as data, the downgrade direction: CSL's element-list engine
8
+ # is strictly richer, so the import covers what the instance format can
9
+ # express and lists everything it dropped in `requires`).
10
+ #
11
+ # Mapped: the info title, the citation/reference variable runs (text
12
+ # elements over the CSL variables, via the crosswalk below), the
13
+ # name-form scalars (initialize-with, name-as-sort-order, et-al
14
+ # lengths), the year-suffix disambiguation, and the delimiters.
15
+ module CslImport
16
+ # CSL variable -> the ISO 690 slot the engine resolves.
17
+ VARIABLE_SLOTS = {
18
+ "author" => "creator", "editor" => "creator",
19
+ "title" => "title", "container-title" => "series",
20
+ "edition" => "edition", "publisher" => "production",
21
+ "publisher-place" => "location", "issued" => "date",
22
+ "DOI" => "identifier", "URL" => "identifier", "PMID" => "identifier",
23
+ "number" => "numeration", "volume" => "numeration",
24
+ "page" => "numeration", "medium" => "medium",
25
+ "accessed" => "access", "version" => "edition",
26
+ }.freeze
27
+
28
+ # CSL features the import drops, reported in `requires` so a resolver
29
+ # fails loudly instead of silently mis-rendering.
30
+ DROPPED_FEATURES = [
31
+ "csl macros (macro elements are not data-expressible)",
32
+ "csl groups with conditional rendering (match= attributes)",
33
+ "csl per-locator templates",
34
+ ].freeze
35
+
36
+ class << self
37
+ # Build a CitationStyle instance from a parsed CSL style JSON.
38
+ #
39
+ # @param csl [Hash] parsed CSL style
40
+ # @return [Style]
41
+ def style(csl)
42
+ Style.new(
43
+ name: style_name(csl),
44
+ family: "iso690",
45
+ requires: dropped_features(csl),
46
+ scheme: scheme(csl),
47
+ templates: templates(csl),
48
+ )
49
+ end
50
+
51
+ # Parse a CSL style JSON string and build the instance.
52
+ #
53
+ # @param [String] json
54
+ # @return [Style]
55
+ def from_json(json)
56
+ style(JSON.parse(json))
57
+ end
58
+
59
+ private
60
+
61
+ def style_name(csl)
62
+ title = csl.dig("info", "title").to_s
63
+ title.empty? ? "Imported CSL style" : "#{title} (CSL import)"
64
+ end
65
+
66
+ def dropped_features(csl)
67
+ feats = DROPPED_FEATURES.dup
68
+ feats << "csl bibliography section" if csl["bibliography"]
69
+ feats << "csl locales" if csl["locales"]
70
+ feats
71
+ end
72
+
73
+ def scheme(csl)
74
+ Style::Scheme.new(
75
+ system: "name-date",
76
+ name_form: name_form(csl),
77
+ locale: Style::Locale.new(no_date: "n.d."),
78
+ )
79
+ end
80
+
81
+ def name_form(csl)
82
+ citation = csl["citation"] || {}
83
+ initialize_with = citation["initialize-with"]
84
+ Style::NameForm.new(
85
+ initials: !initialize_with.nil?,
86
+ initials_separator: initialize_with.is_a?(String) ? initialize_with : " ",
87
+ initials_period: initialize_with.to_s.include?("."),
88
+ inverted_all: citation["name-as-sort-order"].to_s == "all",
89
+ )
90
+ end
91
+
92
+ def templates(csl)
93
+ citation_elements = csl.dig("citation", "elements") || []
94
+ reference_elements = csl.dig("bibliography", "elements") || []
95
+ Style::TemplateMap.new(
96
+ citation: element_template(
97
+ citation_elements, csl.dig("citation", "delimiter") || ", ",
98
+ "{{creator}}, {{date}}{{disambiguator}}",
99
+ ),
100
+ reference: element_template(
101
+ reference_elements, csl.dig("bibliography", "delimiter") || ". ",
102
+ "{{creator}}. {{title}}. {{edition}}. {{series}}. {{production}}, {{date}}. {{identifier}}. {{location}}.",
103
+ ),
104
+ name: "{{surname}}, {{givenNames}}",
105
+ )
106
+ end
107
+
108
+ # Flatten CSL text/variable elements into a slot template. Non-text
109
+ # elements (macros, groups, conditions) contribute nothing: the
110
+ # import covers flat variable runs only.
111
+ def element_template(elements, delimiter, fallback)
112
+ slots = elements.filter_map do |el|
113
+ next unless el.is_a?(Hash)
114
+
115
+ # A CSL text element nests its variable: {"text": {"variable": ...}}
116
+ variable = el.dig("text", "variable") || el["variable"]
117
+ slot = variable && VARIABLE_SLOTS[variable]
118
+ "{{#{slot}}}" if slot
119
+ end
120
+ slots = slots.uniq
121
+ return fallback if slots.empty?
122
+
123
+ slots.join(delimiter)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # Base data element (ISO 690 clause 7). An element wraps typed data
7
+ # taken from the relaton model instance; #present? is the omission
8
+ # predicate, #render the element's formatting method. Punctuation
9
+ # between elements belongs to the kind/style, not the element.
10
+ class Element
11
+ def initialize(model, style:, i18n:, kind: nil, short: false)
12
+ @model = model
13
+ @style = style
14
+ @i18n = i18n
15
+ @kind = kind
16
+ @short = short
17
+ end
18
+
19
+ def present?
20
+ false
21
+ end
22
+
23
+ def render
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ def localized(value)
30
+ value&.content.to_s
31
+ end
32
+
33
+ # Localizable values scoped to the render language: the variants
34
+ # declared in that language, or all of them when none matches
35
+ def titles_in_lang(values)
36
+ titles = Array(values)
37
+ in_lang = titles.select { |t| Array(t.language).include?(@i18n.lang) }
38
+ in_lang.empty? ? titles : in_lang
39
+ end
40
+
41
+ def contributors(role)
42
+ Array(@model.contributor).select { |c| has_role?(c, role) }
43
+ end
44
+
45
+ def has_role?(contributor, role)
46
+ Array(contributor.role).any? do |r|
47
+ r.is_a?(String) ? r == role : r.type == role
48
+ end
49
+ end
50
+
51
+ def org_name(contributor)
52
+ shown = ->(n) do
53
+ text = localized(n)
54
+ @style.scheme.name_form.org_upcase ? text.upcase : text
55
+ end
56
+ name = Array(contributor.organization&.name)
57
+ .map(&shown).join(", ")
58
+ abbrev = localized(contributor.organization&.abbreviation)
59
+ return name if name.empty? || abbrev.empty?
60
+
61
+ "#{name} (#{abbrev})"
62
+ end
63
+
64
+ def item_kind
65
+ @kind || @style.kind_for(@model.type)
66
+ end
67
+
68
+ def person_completename(person)
69
+ localized(person.name&.completename)
70
+ end
71
+
72
+ def person_surname(person)
73
+ localized(person.name&.surname)
74
+ end
75
+
76
+ def person_given(person)
77
+ parts = Array(person.name&.forename).map do |f|
78
+ content = localized(f)
79
+ next content unless content.empty?
80
+ next initial(f.initial) if f.initial.to_s.length == 1
81
+
82
+ ""
83
+ end.reject(&:empty?)
84
+ sep = @style.scheme.name_form.initials_separator.to_s
85
+ return reinitials(localized(person.name&.formatted_initials)) if
86
+ parts.empty?
87
+ return given_with_first_full(parts, sep) if initials?
88
+
89
+ parts.join(" ")
90
+ end
91
+
92
+ # The document-history name form: the first forename cites in
93
+ # full, the rest as initials ("Milena S.")
94
+ def given_with_first_full(parts, sep)
95
+ initials = parts.map { |p| initial(p) }
96
+ return initials.join(sep) unless
97
+ @style.scheme.name_form.subsequent_initials
98
+
99
+ rest = parts.drop(1).map { |p| initial(p) }.join(sep)
100
+ rest.empty? ? parts.first : "#{parts.first}#{sep}#{rest}"
101
+ end
102
+
103
+ # Declared initials are a list in string form; rejoin them with
104
+ # the style's separator
105
+ def reinitials(raw)
106
+ sep = @style.scheme.name_form.initials_separator.to_s
107
+ dots = @style.scheme.name_form.initials_period
108
+ raw.split(/\s+/).reject(&:empty?).map { |i| dots ? i : i.delete(".") }
109
+ .join(sep)
110
+ end
111
+
112
+ def initial(name)
113
+ @style.scheme.name_form.initials_period ? "#{name[0]}." : name[0]
114
+ end
115
+
116
+ def initials?
117
+ @style.scheme.name_form.initials
118
+ end
119
+
120
+ def join_names(names)
121
+ # A style with no join word renders a plain serial list,
122
+ # separated per the locale (the comma, or the CJK 、)
123
+ if @i18n.label("and").empty? && names.size > 1
124
+ sep = @i18n.label("list_separator")
125
+ sep = ", " if sep == "list_separator"
126
+ return names.join(sep)
127
+ end
128
+
129
+ oxford = @i18n.label("oxford_comma")
130
+ if serial_list? && names.size > 1 && !oxford.empty?
131
+ return "#{names[0..-2].join(', ')}#{oxford} " \
132
+ "#{@i18n.label('and')} #{names.last}"
133
+ end
134
+
135
+ case names.size
136
+ when 0 then ""
137
+ when 1 then names.first
138
+ when 2 then names.join(" #{@i18n.label('and')} ")
139
+ else
140
+ "#{names[0..-2].join(', ')}#{oxford} " \
141
+ "#{@i18n.label('and')} #{names.last}"
142
+ end
143
+ end
144
+
145
+ # Serial list style: the oxford comma appears at every gap of a
146
+ # name list, including between two names
147
+ def serial_list?
148
+ @style.scheme.name_form.list_style == "serial"
149
+ end
150
+ end
151
+ end
152
+ end
153
+ 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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
7
+ # The authorizing body of a standard (the IETF RFC Series, the
8
+ # W3C Recommendation process): the organization carrying the
9
+ # authorizer role, cited after the status
10
+ class Authorizer < Element
11
+ def present?
12
+ !authorizer_name.empty?
13
+ end
14
+
15
+ def render
16
+ if (form = authorizer_form) && !form.empty?
17
+ return Template.new(form).evaluate(
18
+ "authorizer" => Template::Field[true, authorizer_name],
19
+ )
20
+ end
21
+
22
+ authorizer_name
23
+ end
24
+
25
+ # The kind's declared form for this slot: the first entry of
26
+ # the kind carrying one (a home variant may shadow the type)
27
+ def authorizer_form
28
+ kind = @style.kind_for(@model.type)
29
+ @style.per_type.find do |t|
30
+ t.type == kind && !t.authorizer.to_s.empty?
31
+ end&.authorizer
32
+ end
33
+
34
+ private
35
+
36
+ def authorizer_name
37
+ contributors("authorizer").map do |c|
38
+ Array(c.organization&.name).map { |n| localized(n) }.join(", ")
39
+ end.reject(&:empty?).first.to_s
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
7
+ # The cited identifier with the item's status beside it and the
8
+ # list comma ("OGC 05-020r27 (Draft)," / "ISO 712,"): the comma
9
+ # stays with the identifier whichever side of the status it
10
+ # follows
11
+ class Citeid < Element
12
+ def present?
13
+ identifier_element.present?
14
+ end
15
+
16
+ def render
17
+ text = identifier_element.render
18
+ stage = status_element.present? ? status_element.render : ""
19
+ stage.empty? ? "#{text}," : "#{text}#{stage},"
20
+ end
21
+
22
+ private
23
+
24
+ def identifier_element
25
+ @identifier_element ||=
26
+ Identifier.new(@model, style: @style, i18n: @i18n, kind: @kind,
27
+ short: @short)
28
+ end
29
+
30
+ def status_element
31
+ @status_element ||=
32
+ Status.new(@model, style: @style, i18n: @i18n, kind: @kind,
33
+ short: @short)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Elements
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.
11
+ class ComponentPart < Element
12
+ HOST_RELATION_TYPES = %w[partOf includedIn].freeze
13
+
14
+ def present?
15
+ !host_title_text.empty?
16
+ end
17
+
18
+ def render
19
+ "#{lead}#{host_title}#{host_editors}#{host_edition}" \
20
+ "#{host_production}"
21
+ end
22
+
23
+ private
24
+
25
+ def lead
26
+ label = @i18n.label("in")
27
+ label.empty? ? "" : "#{label} "
28
+ end
29
+
30
+ def relation
31
+ Array(@model.relation).find do |r|
32
+ HOST_RELATION_TYPES.include?(r.type)
33
+ end
34
+ end
35
+
36
+ def host
37
+ relation&.bibitem
38
+ end
39
+
40
+ def host_title_text
41
+ return localized(relation&.description) if host.nil?
42
+
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
77
+ end
78
+
79
+ def host_edition
80
+ return "" if host.nil?
81
+
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?
89
+
90
+ production = Production.new(host, style: @style, i18n: @i18n)
91
+ production.present? ? ". #{production.render}" : ""
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end