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,48 @@
1
+ lang: en
2
+ labels:
3
+ edition: "ed"
4
+ ordinal_word_1: "First"
5
+ ordinal_word_2: "Second"
6
+ ordinal_word_3: "Third"
7
+ ordinal_word_4: "Fourth"
8
+ ordinal_word_5: "Fifth"
9
+ ordinal_word_6: "Sixth"
10
+ ordinal_word_7: "Seventh"
11
+ ordinal_word_8: "Eighth"
12
+ ordinal_word_9: "Ninth"
13
+ ordinal_word_10: "Tenth"
14
+ size_volume: "vol."
15
+ size_page: "pp."
16
+ page: "p. %"
17
+ pages: "pp. %"
18
+ volume: "vol. %"
19
+ issue: "no. %"
20
+ no_place: "n.p."
21
+ version: "Version"
22
+ medium_website: "[website]"
23
+ viewed: "viewed"
24
+ month_1: "January"
25
+ month_2: "February"
26
+ month_3: "March"
27
+ month_4: "April"
28
+ month_5: "May"
29
+ month_6: "June"
30
+ month_7: "July"
31
+ month_8: "August"
32
+ month_9: "September"
33
+ month_10: "October"
34
+ month_11: "November"
35
+ month_12: "December"
36
+
37
+ series_no: "no."
38
+ report_no: "Report no."
39
+ available_from: "Available from:"
40
+ at_url: "At:"
41
+ in: "In:"
42
+ and: "and"
43
+ oxford_comma: ","
44
+ others: "et al."
45
+ ed: "ed."
46
+ eds: "eds."
47
+ date_range: "–"
48
+ punct: {}
@@ -0,0 +1,14 @@
1
+ lang: fr
2
+ labels:
3
+ edition: "éd"
4
+ series_no: "n°"
5
+ report_no: "Rapport no."
6
+ available_from: "Disponible sur :"
7
+ in: "Dans :"
8
+ and: "et"
9
+ oxford_comma: ""
10
+ ed: "éd."
11
+ eds: "éd."
12
+ date_range: "–"
13
+ punct:
14
+ production_sep: " : "
@@ -0,0 +1,38 @@
1
+ lang: ja
2
+ labels:
3
+ edition: "版"
4
+ size_volume: "巻"
5
+ size_page: "頁"
6
+ page: "%頁"
7
+ pages: "%頁"
8
+ volume: "巻%"
9
+ issue: "%号"
10
+ version: "版"
11
+ viewed: "閲覧"
12
+ series_no: "番号"
13
+ report_no: "報告番号"
14
+ available_from: "入手先:"
15
+ at_url: "URL:"
16
+ in: "収録:"
17
+ ed: "編"
18
+ eds: "編"
19
+ updated: "更新"
20
+ no_place: "出版地不明"
21
+ edition_cardinal: "第{{ var1 }}版"
22
+ edition_ordinal: "第{{ var1 }}版"
23
+ version_cardinal: "第{{ var1 }}版"
24
+ in_series: "シリーズ"
25
+ list_separator: "、 "
26
+ and: ""
27
+ oxford_comma: ""
28
+ date_range: "〜"
29
+ punct:
30
+ size-join: ""
31
+ marker-space: ""
32
+ production_sep: ": "
33
+ cjk-latin-separator: "、 "
34
+ period: "。"
35
+ comma: "、"
36
+ colon: ":"
37
+ open-paren: "("
38
+ close-paren: ")"
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Relaton
6
+ module Render
7
+ module Iso690
8
+ # Language-specific labels and typographic punctuation, declared per
9
+ # language in i18n/<lang>.yml. Adding a language adds a YAML
10
+ # declaration — no code. A style instance's localized strings overlay
11
+ # the language pack; language-varying intra-element punctuation
12
+ # (e.g. French " : " production separator) is declared under
13
+ # `punct:`.
14
+ class I18n < Lutaml::Model::Serializable
15
+ attribute :lang, :string, default: "en"
16
+ attribute :labels, :hash, default: {}
17
+ attribute :punct, :hash, default: {}
18
+
19
+ key_value do
20
+ map "lang", to: :lang
21
+ map "labels", to: :labels
22
+ map "punct", to: :punct
23
+ end
24
+
25
+ def label(key)
26
+ labels.fetch(key.to_s, key.to_s)
27
+ end
28
+
29
+ def punct_fetch(key, default)
30
+ punct.fetch(key, default)
31
+ end
32
+
33
+ # Style-instance localized strings win over the language pack.
34
+ def overlay!(locale)
35
+ labels.merge!(locale.label_map)
36
+ punct.merge!(locale.punct || {})
37
+ self
38
+ end
39
+
40
+ # Runtime label overrides (a caller's i18n hash): the pack's label
41
+ # names win as the key mapping; unknown keys are ignored.
42
+ def overlay_hash!(hash)
43
+ known = %w[
44
+ edition series_no report_no available_from in and oxford_comma
45
+ ed eds date_range others no_date no_author at
46
+ edition_ordinal edition_cardinal version_cardinal
47
+ ]
48
+ hash.each do |key, value|
49
+ labels[key.to_s] = value if known.include?(key.to_s) && !value.nil?
50
+ end
51
+ self
52
+ end
53
+
54
+ class << self
55
+ def load(lang = "en")
56
+ code = lang.to_s.empty? ? "en" : lang
57
+ path = File.join(__dir__, "i18n", "#{code}.yml")
58
+ unless File.file?(path)
59
+ # 1.x carried declarations for every locale isodoc uses; the
60
+ # v2 packs are per-style data. Unknown languages fall back to
61
+ # the English pack rather than killing the whole conversion.
62
+ fallback_warn(code)
63
+ path = File.join(__dir__, "i18n", "en.yml")
64
+ raise ArgumentError, "no i18n declarations for language #{code}" unless File.file?(path)
65
+ # The pack supplies the labels; the requested language still
66
+ # governs localization (bilingual titles select by it)
67
+ return from_yaml(File.read(path)).tap { |i| i.lang = code }
68
+ end
69
+
70
+ from_yaml(File.read(path))
71
+ end
72
+
73
+ def fallback_warn(code)
74
+ (@fallback_warned ||= {})[code] and return
75
+ @fallback_warned[code] = true
76
+ warn "relaton-render: no i18n declarations for language " \
77
+ "#{code}; falling back to en"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # Standard and persistent identifiers (ISO 690 clause 7.11): each
7
+ # identifier kind carries its own rendering method. Kinds are keyed by
8
+ # the docidentifier's type attribute — never guessed from content.
9
+ # Flavors register additional kinds (e.g. Pubid::Iala's MRN).
10
+ module IdentifierKinds
11
+ KINDS = {
12
+ "DOI" => ->(content) { "https://doi.org/#{content}" },
13
+ "ISBN" => ->(content) { "ISBN #{content}" },
14
+ "ISSN" => ->(content) { "ISSN #{content}" },
15
+ "URN" => ->(content) { content },
16
+ "MRN" => ->(content) { content },
17
+ }.freeze
18
+
19
+ class << self
20
+ def render(docidentifier)
21
+ content = docidentifier.content.to_s
22
+ kind = KINDS[docidentifier.type]
23
+ kind ? kind.call(content) : content
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # A bibliographic index: the ordered reference list, laid out per the
7
+ # BibliographicStyle instance. Sorting follows the style's declared
8
+ # sortKey rules (creator, then date); numbering is layout data.
9
+ class IndexRenderer
10
+ def initialize(style: "author-date", lang: "en", script: "Latn")
11
+ @style = Style.load(style)
12
+ @i18n = I18n.load(lang).overlay!(@style.scheme.locale)
13
+ @renderer = Renderer.new(style: style, lang: lang, script: script)
14
+ end
15
+
16
+ # The reference strings, sorted per the style's sortKey rules.
17
+ def references(models)
18
+ sort_rules = @style.sort_keys
19
+ fields = models.to_h { |m| [m, field_table(m)] }
20
+ models.sort do |a, b|
21
+ fa = fields[a]
22
+ fb = fields[b]
23
+ sort_rules.each do |rule|
24
+ cmp = key_of(fa, rule) <=> key_of(fb, rule)
25
+ next if cmp.zero?
26
+
27
+ break(rule.descending? ? -cmp : cmp)
28
+ end || 0
29
+ end.map { |model| @renderer.render(model) }
30
+ end
31
+
32
+ # The laid-out index: numbered entries per the layout rules
33
+ # (arabic by default; "none" renders bare references).
34
+ def render(models, numbering: "arabic")
35
+ refs = references(models)
36
+ return refs if numbering == "none"
37
+
38
+ refs.each_with_index.map { |ref, i| "#{i + 1}. #{ref}" }
39
+ end
40
+
41
+ private
42
+
43
+ def field_table(model)
44
+ Fields.new(model, style: @style, i18n: @i18n).to_h
45
+ end
46
+
47
+ def key_of(fields, rule)
48
+ field = fields[rule.attribute.to_s.delete("_").downcase]
49
+ return "" if field.nil?
50
+
51
+ text = field.respond_to?(:text) ? field.text : field.to_s
52
+ text.downcase
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # The vocabulary mapping from a bibliographic item's wire type value
7
+ # to its ISO 690 clause 8 resource kind (TemplateType). This table is
8
+ # the engine's charter — style instances select perType templates on
9
+ # kinds and never enumerate type synonyms. Unmapped values fall to
10
+ # the report kind (clause 8.11), the runner's fallback.
11
+ module Kinds
12
+ TO_KIND = {
13
+ "book" => "monograph",
14
+ "booklet" => "monograph",
15
+ "manual" => "monograph",
16
+ "proceedings" => "monograph",
17
+ "thesis" => "monograph",
18
+ "standard" => "report",
19
+ "techreport" => "report",
20
+ "report" => "report",
21
+ "patent" => "patent",
22
+ "journal" => "continuing",
23
+ "serial" => "continuing",
24
+ "article-journal" => "continuing",
25
+ "article-magazine" => "continuing",
26
+ "inbook" => "component_part",
27
+ "incollection" => "component_part",
28
+ "inproceedings" => "component_part",
29
+ "article" => "serial_part",
30
+ "software" => "online",
31
+ "unpublished" => "online",
32
+ "dataset" => "online",
33
+ "website" => "webdoc",
34
+ "webresource" => "webdoc",
35
+ "webpage" => "online",
36
+ "online" => "online",
37
+ }.freeze
38
+
39
+ DEFAULT_KIND = "report"
40
+
41
+ class << self
42
+ def mapped?(type)
43
+ TO_KIND.key?(type.to_s)
44
+ end
45
+ end
46
+
47
+ def self.kind_for(type)
48
+ TO_KIND.fetch(type.to_s, DEFAULT_KIND)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # Public API: relaton model in, citation string out. The style
7
+ # instance supplies every rendering decision; this class only wires
8
+ # the template evaluator to the field resolver. No XML, no
9
+ # intermediate hashes.
10
+ class Renderer
11
+ DEFAULT_STYLE = "author-date"
12
+
13
+ class << self
14
+ def render(model, style: DEFAULT_STYLE, lang: "en", script: "Latn",
15
+ elements: {})
16
+ new(style: style, lang: lang, script: script,
17
+ elements: elements).render(model)
18
+ end
19
+
20
+ def citation(model, disambiguator: nil, style: DEFAULT_STYLE,
21
+ lang: "en", script: "Latn")
22
+ new(style: style, lang: lang, script: script)
23
+ .citation(model, disambiguator: disambiguator)
24
+ end
25
+ end
26
+
27
+ def initialize(style: DEFAULT_STYLE, lang: "en", script: "Latn",
28
+ labels: {}, elements: {})
29
+ @style = Style.load(style)
30
+ @elements = Rules.resolve(@style.rules, elements)
31
+ @i18n = I18n.load(lang)
32
+ @i18n.overlay_hash!(labels) unless labels.empty?
33
+ # The style instance is the citation-style authority: its
34
+ # declared strings win over the caller's generic i18n hash
35
+ @i18n.overlay!(@style.scheme.locale)
36
+ @script = script
37
+ end
38
+
39
+ def render(model, disambiguator: nil)
40
+ kind = @style.kind_for(model.type)
41
+ template = @style.template_for(kind, home: home_docid?(model))
42
+ out = Template.new(template).evaluate(
43
+ Fields.new(model, style: @style, i18n: @i18n,
44
+ disambiguator: disambiguator,
45
+ elements: @elements).to_h,
46
+ )
47
+ raise ::Relaton::Render::Unrenderable,
48
+ "no renderable elements" if out.strip.empty?
49
+
50
+ out
51
+ end
52
+
53
+ def home_docid?(model)
54
+ types = Array(@style.scheme.home_docid_type)
55
+ return false if types.empty?
56
+
57
+ Array(model.docidentifier).any? { |d| types.include?(d.type) }
58
+ end
59
+
60
+ # A creator list alone (flavors' document-history name forms)
61
+ def render_creators(model)
62
+ Elements.build(:creator, model, style: @style, i18n: @i18n,
63
+ short: true, elements: @elements).render
64
+ end
65
+
66
+ # The short cite: the reference with the first-biblio marker
67
+ def render_short(model, delim)
68
+ kind = @style.kind_for(model.type)
69
+ template = @style.short_template_for(kind,
70
+ home: home_docid?(model)) ||
71
+ @style.template_for(kind, home: home_docid?(model))
72
+ Template.new(template).evaluate_short(
73
+ Fields.new(model, style: @style, i18n: @i18n, short: true,
74
+ elements: @elements).to_h,
75
+ delim,
76
+ )
77
+ rescue ::Relaton::Render::Unrenderable
78
+ ""
79
+ end
80
+
81
+ # The in-text creator with its et-al truncation: the
82
+ # author-date tag's author part
83
+ def in_text_author(model)
84
+ Elements.build(:creator, model, style: @style, i18n: @i18n,
85
+ elements: @elements).in_text
86
+ end
87
+
88
+ # The author-date in-text capsule ("Aluffi <em>et al.</em>
89
+ # 2022a"): the in-text creator and the disambiguated date
90
+ def author_date_citation(model, disambiguator: nil)
91
+ f = Fields.new(model, style: @style, i18n: @i18n,
92
+ disambiguator: disambiguator,
93
+ elements: @elements).to_h
94
+ [in_text_author(model), f["disambiguateddate"].text]
95
+ .reject(&:empty?).join(" ")
96
+ end
97
+
98
+ # The bracketed author-date in-text capsule ("Aluffi (2022)"):
99
+ # the author and the PLAIN date (un-disambiguated, the 1.x
100
+ # citetemplate's {{ date }}) in the locale's parentheses
101
+ def bracketed_date_citation(model, **)
102
+ f = Fields.new(model, style: @style, i18n: @i18n,
103
+ elements: @elements).to_h
104
+ date = f["date"].text
105
+ return "" if date.empty?
106
+
107
+ open = @i18n.punct_fetch("open-paren", "(")
108
+ close = @i18n.punct_fetch("close-paren", ")")
109
+ [in_text_author(model), "#{open}#{date}#{close}"]
110
+ .reject(&:empty?).join(" ")
111
+ end
112
+
113
+ # The disambiguated date alone (the citation renderings' date)
114
+ def disambiguated_date(model, disambiguator: nil)
115
+ Fields.new(model, style: @style, i18n: @i18n,
116
+ disambiguator: disambiguator,
117
+ elements: @elements).to_h["disambiguateddate"].text
118
+ end
119
+
120
+ def short_from_reference?
121
+ @style.scheme.short_from_reference
122
+ end
123
+
124
+ # The title citation ("{{ title }}" in the 1.x citetemplate):
125
+ # the title element's plain form, no style markers
126
+ def title_citation(model)
127
+ Elements.build(:title, model, style: @style, i18n: @i18n,
128
+ elements: @elements).plain
129
+ end
130
+
131
+ # The in-text author key (the principal creator's surname, as
132
+ # the citation renders it): the batch disambiguation groups by
133
+ # it
134
+ def author_key(model)
135
+ Fields.new(model, style: @style, i18n: @i18n,
136
+ elements: @elements).to_h["surname"].text
137
+ end
138
+
139
+ # In-text form: SURNAME, year + disambiguator (name-and-date)
140
+ def citation(model, disambiguator: nil)
141
+ Template.new(@style.templates.citation).evaluate(
142
+ Fields.new(model, style: @style, i18n: @i18n,
143
+ disambiguator: disambiguator,
144
+ elements: @elements).to_h,
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ module Rules
7
+ # The BIPM rule set, moved verbatim from the metanorma-bipm
8
+ # flavor's BipmElements: the affiliation-free organization
9
+ # creator, the bold-volume journal numeration, the
10
+ # no-place-with-publisher production, the separator-carrying
11
+ # edition and medium, and the component-part host order.
12
+ # Select from pack data:
13
+ # rules: { creator: bipm_creator, ... }
14
+ module Bipm
15
+ # The BIPM component-part order: the host editors before the
16
+ # host title, the host medium after it, and the production
17
+ # parenthesized with its own trailing period
18
+ class BipmComponentPart < Elements::ComponentPart
19
+ def render
20
+ h = host or return ""
21
+ out = +"#{lead}#{host_names(h)} (#{eds_label(h)})"
22
+ out += " #{host_title}" unless host_title_text.empty?
23
+ out += medium_text(h)
24
+ out += production_text(h)
25
+ out
26
+ end
27
+
28
+ private
29
+
30
+ def host_editors(h)
31
+ Array(h.contributor).select { |c| has_role?(c, "editor") }
32
+ end
33
+
34
+ def eds_label(h)
35
+ @i18n.label(host_editors(h).one? ? "ed" : "eds")
36
+ end
37
+
38
+ def host_names(h)
39
+ names = host_editors(h).map { |c|
40
+ person = c.person or next ""
41
+
42
+ [person_surname(person), person_given(person)].reject(&:empty?)
43
+ .join(" ")
44
+ }.reject(&:empty?)
45
+ join_names(names)
46
+ end
47
+
48
+ def medium_text(h)
49
+ BipmMedium.new(h, style: @style, i18n: @i18n).render.to_s
50
+ end
51
+
52
+ def production_text(h)
53
+ production = Elements::Production
54
+ .new(h, style: @style, i18n: @i18n).render.to_s
55
+ production.empty? ? "" : " (#{production})."
56
+ end
57
+ end
58
+
59
+ # The BIPM journal numeration: the volume bold and the issue
60
+ # in parens, pages unlabelled. Separate extent elements join
61
+ # with semicolons, localities within one element with spaces
62
+ class BipmExtent < Elements::Extent
63
+ def render
64
+ groups = extent_locality_groups.map { |g| group_text(g) }
65
+ .reject(&:empty?)
66
+ groups.join("; ")
67
+ end
68
+
69
+ private
70
+
71
+ def extent_locality_groups
72
+ Array(@model.extent).map do |e|
73
+ Array(e.locality) +
74
+ Array(e.locality_stack).flat_map { |s| Array(s.locality) }
75
+ end
76
+ end
77
+
78
+ def group_text(localities)
79
+ volume = plain_locality(localities, "volume")
80
+ volume = volume.empty? ? "" : "<strong>#{volume}</strong>"
81
+ issue = plain_locality(localities, "issue")
82
+ issue = issue.empty? ? "" : "(#{issue})"
83
+ [volume, issue, page_text(localities)].reject(&:empty?).join(" ")
84
+ end
85
+
86
+ def plain_locality(localities, type)
87
+ loc = localities.find { |l| l.type == type } or return ""
88
+ loc.reference_from.to_s
89
+ end
90
+
91
+ def page_text(localities)
92
+ loc = localities.find { |l| l.type == "page" } or return ""
93
+ from = loc.reference_from.to_s
94
+ to = loc.reference_to.to_s
95
+ text = to.empty? || to == from ? from :
96
+ "#{from}#{@i18n.label('date_range')}#{to}"
97
+ return text if item_kind == "serial_part"
98
+
99
+ label = @i18n.label(to.empty? || to == from ? "page" : "pages")
100
+ # the page labels are %-templates ("pp. %"); the block
101
+ # form keeps a % in the cited text from re-expanding
102
+ label.include?("%") ? label.gsub("%") { text } : "#{label} #{text}"
103
+ end
104
+ end
105
+
106
+ # The BIPM production: the no-place placeholder stands only
107
+ # with a publisher ("(n.p.: Publisher)"); a nameless
108
+ # publisher drops the production entirely
109
+ class BipmProduction < Elements::Production
110
+ private
111
+
112
+ def no_place_production
113
+ return "" if publishers.empty?
114
+
115
+ super
116
+ end
117
+ end
118
+
119
+ # The serial volume, bold, from the size's volume value
120
+ class BipmVolume < Element
121
+ def present?
122
+ !render.empty?
123
+ end
124
+
125
+ def render
126
+ volume = Array(@model.size&.value).find do |v|
127
+ !v.is_a?(String) && !v.is_a?(Hash) && v.type == "volume"
128
+ end or return ""
129
+
130
+ "<strong>#{volume.content}</strong>"
131
+ end
132
+ end
133
+
134
+ # The BIPM medium, verbatim and bracketed with its leading
135
+ # separator (" [Online]", " [dataset]") — an absent medium
136
+ # attaches to nothing
137
+ class BipmMedium < Elements::Medium
138
+ private
139
+
140
+ def medium
141
+ m = @model.medium or return ""
142
+ text = m.carrier.to_s
143
+ text = m.genre.to_s if text.empty?
144
+ text = [m.form.to_s, m.size.to_s].reject(&:empty?)
145
+ .join(", ") if text.empty?
146
+ text.empty? ? "" : " [#{text}]"
147
+ end
148
+ end
149
+
150
+ # The BIPM edition carries its own leading separator (",
151
+ # First edition" for monographs, ". Version 2" online), so
152
+ # its absence drops the separator with it
153
+ class BipmEdition < Elements::Edition
154
+ def render
155
+ text = super.to_s
156
+ sep = item_kind == "monograph" ? ", " : ". "
157
+ text.empty? ? "" : "#{sep}#{text}"
158
+ end
159
+ end
160
+
161
+ # The BIPM organization creator: the name alone, no
162
+ # abbreviation suffix. Standards without personal creators
163
+ # cite their publisher's name; monographs cite nothing
164
+ class BipmCreator < Elements::Creator
165
+ private
166
+
167
+ def org_name(contributor)
168
+ Array(contributor.organization&.name).map { |n| localized(n) }
169
+ .join(", ")
170
+ end
171
+
172
+ def fallback_name
173
+ return "" if %w[monograph continuing component_part]
174
+ .include?(@style.kind_for(@model.type))
175
+
176
+ publishers = contributors("publisher").filter_map do |c|
177
+ localized(Array(c.organization&.name).first)
178
+ end
179
+ publishers.reject(&:empty?).first.to_s
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end