relaton-render 0.4.2 → 3.0.0.pre.alpha.26

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +6 -1
  3. data/.github/workflows/release.yml +24 -0
  4. data/.gitignore +14 -0
  5. data/.rubocop.yml +2 -2
  6. data/Gemfile +17 -1
  7. data/README.adoc +13 -258
  8. data/docs/citation-style-pack.adoc +370 -0
  9. data/lib/relaton/render/general.rb +305 -0
  10. data/lib/relaton/render/iso690/element.rb +153 -0
  11. data/lib/relaton/render/iso690/elements/access.rb +44 -0
  12. data/lib/relaton/render/iso690/elements/authorizer.rb +30 -0
  13. data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
  14. data/lib/relaton/render/iso690/elements/component_part.rb +99 -0
  15. data/lib/relaton/render/iso690/elements/creator.rb +258 -0
  16. data/lib/relaton/render/iso690/elements/edition.rb +92 -0
  17. data/lib/relaton/render/iso690/elements/extent.rb +71 -0
  18. data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
  19. data/lib/relaton/render/iso690/elements/location.rb +73 -0
  20. data/lib/relaton/render/iso690/elements/medium.rb +32 -0
  21. data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
  22. data/lib/relaton/render/iso690/elements/production.rb +92 -0
  23. data/lib/relaton/render/iso690/elements/pub_date.rb +56 -0
  24. data/lib/relaton/render/iso690/elements/series.rb +66 -0
  25. data/lib/relaton/render/iso690/elements/size.rb +41 -0
  26. data/lib/relaton/render/iso690/elements/status.rb +30 -0
  27. data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
  28. data/lib/relaton/render/iso690/elements/title.rb +56 -0
  29. data/lib/relaton/render/iso690/elements/updated.rb +38 -0
  30. data/lib/relaton/render/iso690/elements.rb +67 -0
  31. data/lib/relaton/render/iso690/fields.rb +142 -0
  32. data/lib/relaton/render/iso690/i18n/en.yml +47 -0
  33. data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
  34. data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
  35. data/lib/relaton/render/iso690/i18n.rb +83 -0
  36. data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
  37. data/lib/relaton/render/iso690/index_renderer.rb +57 -0
  38. data/lib/relaton/render/iso690/kinds.rb +53 -0
  39. data/lib/relaton/render/iso690/renderer.rb +148 -0
  40. data/lib/relaton/render/iso690/rules.rb +36 -0
  41. data/lib/relaton/render/iso690/style/locale.rb +53 -0
  42. data/lib/relaton/render/iso690/style/name_form.rb +87 -0
  43. data/lib/relaton/render/iso690/style/scheme.rb +33 -0
  44. data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
  45. data/lib/relaton/render/iso690/style/template_map.rb +40 -0
  46. data/lib/relaton/render/iso690/style/type_template.rb +37 -0
  47. data/lib/relaton/render/iso690/style.rb +222 -0
  48. data/lib/relaton/render/iso690/styles/apa.yml +45 -0
  49. data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
  50. data/lib/relaton/render/iso690/styles/chicago.yml +48 -0
  51. data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
  52. data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
  53. data/lib/relaton/render/iso690/template.rb +248 -0
  54. data/lib/relaton/render/iso690.rb +27 -0
  55. data/lib/relaton/render/version.rb +1 -1
  56. data/lib/relaton-render.rb +11 -5
  57. data/relaton-render.gemspec +6 -13
  58. metadata +61 -116
  59. data/lib/isodoc/i18n.rb +0 -33
  60. data/lib/isodoc-yaml/i18n-ar.yaml +0 -44
  61. data/lib/isodoc-yaml/i18n-de.yaml +0 -43
  62. data/lib/isodoc-yaml/i18n-en.yaml +0 -40
  63. data/lib/isodoc-yaml/i18n-es.yaml +0 -47
  64. data/lib/isodoc-yaml/i18n-fr.yaml +0 -48
  65. data/lib/isodoc-yaml/i18n-ru.yaml +0 -51
  66. data/lib/isodoc-yaml/i18n-zh-Hans.yaml +0 -40
  67. data/lib/relaton/render/fields/date.rb +0 -45
  68. data/lib/relaton/render/fields/fields.rb +0 -245
  69. data/lib/relaton/render/general/citations.rb +0 -96
  70. data/lib/relaton/render/general/config.yml +0 -79
  71. data/lib/relaton/render/general/render.rb +0 -185
  72. data/lib/relaton/render/general/render_classes.rb +0 -129
  73. data/lib/relaton/render/parse/parse.rb +0 -64
  74. data/lib/relaton/render/parse/parse_contributors.rb +0 -132
  75. data/lib/relaton/render/parse/parse_extract.rb +0 -180
  76. data/lib/relaton/render/template/liquid.rb +0 -15
  77. data/lib/relaton/render/template/template.rb +0 -188
  78. data/lib/relaton/render/utils/utils.rb +0 -27
@@ -0,0 +1,148 @@
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
+ new(style: style, lang: lang, script: script).render(model)
16
+ end
17
+
18
+ def citation(model, disambiguator: nil, style: DEFAULT_STYLE,
19
+ lang: "en", script: "Latn")
20
+ new(style: style, lang: lang, script: script)
21
+ .citation(model, disambiguator: disambiguator)
22
+ end
23
+ end
24
+
25
+ def initialize(style: DEFAULT_STYLE, lang: "en", script: "Latn",
26
+ labels: {}, elements: {})
27
+ @style = Style.load(style)
28
+ @elements = Rules.resolve(@style.rules, elements)
29
+ @i18n = I18n.load(lang)
30
+ @i18n.overlay_hash!(labels) unless labels.empty?
31
+ # The style instance is the citation-style authority: its
32
+ # declared strings win over the caller's generic i18n hash
33
+ @i18n.overlay!(@style.scheme.locale)
34
+ @script = script
35
+ end
36
+
37
+ def render(model, disambiguator: nil)
38
+ kind = @style.kind_for(model.type)
39
+ template = @style.template_for(kind, home: home_docid?(model))
40
+ out = Template.new(template).evaluate(
41
+ Fields.new(model, style: @style, i18n: @i18n,
42
+ disambiguator: disambiguator,
43
+ elements: @elements).to_h,
44
+ )
45
+ raise ::Relaton::Render::Unrenderable,
46
+ "no renderable elements" if out.strip.empty?
47
+
48
+ out
49
+ end
50
+
51
+ def home_docid?(model)
52
+ types = Array(@style.scheme.home_docid_type)
53
+ return false if types.empty?
54
+
55
+ Array(model.docidentifier).any? { |d| types.include?(d.type) }
56
+ end
57
+
58
+ # A creator list alone (flavors' document-history name forms)
59
+ def render_creators(model)
60
+ Elements.build(:creator, model, style: @style, i18n: @i18n,
61
+ short: true, elements: @elements).render
62
+ end
63
+
64
+ # The short cite: the reference with the first-biblio marker
65
+ def render_short(model, delim)
66
+ kind = @style.kind_for(model.type)
67
+ template = @style.short_template_for(kind,
68
+ home: home_docid?(model)) ||
69
+ @style.template_for(kind, home: home_docid?(model))
70
+ Template.new(template).evaluate_short(
71
+ Fields.new(model, style: @style, i18n: @i18n, short: true,
72
+ elements: @elements).to_h,
73
+ delim,
74
+ )
75
+ rescue ::Relaton::Render::Unrenderable
76
+ ""
77
+ end
78
+
79
+ # The in-text creator with its et-al truncation: the
80
+ # author-date tag's author part
81
+ def in_text_author(model)
82
+ Elements.build(:creator, model, style: @style, i18n: @i18n,
83
+ elements: @elements).in_text
84
+ end
85
+
86
+ # The author-date in-text capsule ("Aluffi <em>et al.</em>
87
+ # 2022a"): the in-text creator and the disambiguated date
88
+ def author_date_citation(model, disambiguator: nil)
89
+ f = Fields.new(model, style: @style, i18n: @i18n,
90
+ disambiguator: disambiguator,
91
+ elements: @elements).to_h
92
+ [in_text_author(model), f["disambiguateddate"].text]
93
+ .reject(&:empty?).join(" ")
94
+ end
95
+
96
+ # The bracketed author-date in-text capsule ("Aluffi (2022)"):
97
+ # the author and the PLAIN date (un-disambiguated, the 1.x
98
+ # citetemplate's {{ date }}) in the locale's parentheses
99
+ def bracketed_date_citation(model, **)
100
+ f = Fields.new(model, style: @style, i18n: @i18n,
101
+ elements: @elements).to_h
102
+ date = f["date"].text
103
+ return "" if date.empty?
104
+
105
+ open = @i18n.punct_fetch("open-paren", "(")
106
+ close = @i18n.punct_fetch("close-paren", ")")
107
+ [in_text_author(model), "#{open}#{date}#{close}"]
108
+ .reject(&:empty?).join(" ")
109
+ end
110
+
111
+ # The disambiguated date alone (the citation renderings' date)
112
+ def disambiguated_date(model, disambiguator: nil)
113
+ Fields.new(model, style: @style, i18n: @i18n,
114
+ disambiguator: disambiguator,
115
+ elements: @elements).to_h["disambiguateddate"].text
116
+ end
117
+
118
+ def short_from_reference?
119
+ @style.scheme.short_from_reference
120
+ end
121
+
122
+ # The title citation ("{{ title }}" in the 1.x citetemplate):
123
+ # the title element's plain form, no style markers
124
+ def title_citation(model)
125
+ Elements.build(:title, model, style: @style, i18n: @i18n,
126
+ elements: @elements).plain
127
+ end
128
+
129
+ # The in-text author key (the principal creator's surname, as
130
+ # the citation renders it): the batch disambiguation groups by
131
+ # it
132
+ def author_key(model)
133
+ Fields.new(model, style: @style, i18n: @i18n,
134
+ elements: @elements).to_h["surname"].text
135
+ end
136
+
137
+ # In-text form: SURNAME, year + disambiguator (name-and-date)
138
+ def citation(model, disambiguator: nil)
139
+ Template.new(@style.templates.citation).evaluate(
140
+ Fields.new(model, style: @style, i18n: @i18n,
141
+ disambiguator: disambiguator,
142
+ elements: @elements).to_h,
143
+ )
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # The named-rule registry: presentation-of-models implementations
7
+ # the engine ships once, addressable from pack data as
8
+ # rules: { slot: rule_name }. Flavors migrate their ELEMENTS maps
9
+ # onto registered rules; nothing references a flavor class.
10
+ module Rules
11
+ REGISTRY = {}
12
+
13
+ class << self
14
+ def register(name, klass)
15
+ REGISTRY[name.to_s] = klass
16
+ end
17
+
18
+ # A pack's rule selections become the renderer's element map;
19
+ # caller-supplied elements win over the pack's selections
20
+ def resolve(rules, elements = {})
21
+ Array(rules).to_h { |slot, name| [slot.to_sym, fetch(name)] }
22
+ .merge(elements || {})
23
+ end
24
+
25
+ private
26
+
27
+ def fetch(name)
28
+ REGISTRY.fetch(name.to_s) do
29
+ raise ArgumentError, "unknown citation style rule #{name}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # LocalizedStrings: per-style overrides of the language pack's
8
+ # connective strings, keyed as in the CitationStyle model.
9
+ class Locale < Lutaml::Model::Serializable
10
+ attribute :conj, :string
11
+ attribute :others, :string
12
+ attribute :no_date, :string
13
+ attribute :no_author, :string
14
+ attribute :in_str, :string
15
+ attribute :at, :string
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: -> { {} }
20
+ # Typographic punctuation overrides (production order and
21
+ # separator), declared as in the language packs
22
+ attribute :punct, :hash, default: -> { {} }
23
+
24
+ key_value do
25
+ map "and", to: :conj
26
+ map "others", to: :others
27
+ map "noDate", to: :no_date
28
+ map "noAuthor", to: :no_author
29
+ map "in", to: :in_str
30
+ map "at", to: :at
31
+ map "availableAt", to: :available_at
32
+ map "labels", to: :labels
33
+ map "punct", to: :punct
34
+ end
35
+
36
+ LABEL_FOR = {
37
+ "conj" => "and", "others" => "others", "no_date" => "no_date",
38
+ "no_author" => "no_author", "in_str" => "in", "at" => "at",
39
+ "available_at" => "available_from",
40
+ }.freeze
41
+
42
+ def label_map
43
+ base = LABEL_FOR.filter_map do |attr, label|
44
+ value = public_send(attr)
45
+ [label, value] unless value.nil?
46
+ end.to_h
47
+ base.merge(labels || {})
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # How personal names are formed, as declared by the style's
8
+ # citation scheme (relaton-models NameFormRules).
9
+ class NameForm < Lutaml::Model::Serializable
10
+ attribute :initials, :boolean, default: -> { false }
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 }
20
+ # Every personal name takes the inverted form (surname first),
21
+ # not just the principal creator
22
+ attribute :inverted_all, :boolean, default: -> { false }
23
+ # "serial": the oxford comma appears at every gap of the
24
+ # creator list, including between two names
25
+ attribute :list_style, :string, default: ""
26
+ # Organization names take the inverted form's casing; small-cap
27
+ # styles keep the declared case
28
+ attribute :org_upcase, :boolean, default: -> { true }
29
+ # When no personal creator exists, the creator slot falls back
30
+ # to the publisher's abbreviation ("ISO: ISO 712, ...")
31
+ attribute :creator_fallback, :string
32
+ # A lone editor still carries the role marker ("(ed.)")
33
+ attribute :lone_editor_marked, :boolean, default: -> { false }
34
+ # Initials without their trailing periods ("Nixon RM")
35
+ attribute :initials_period, :boolean, default: -> { true }
36
+ # The inverted form's surname casing: the name-date convention
37
+ # upcases; small-cap styles keep the declared case
38
+ attribute :surname_upcase, :boolean, default: -> { true }
39
+ # Initials are a list; this separates them ("J. K." vs "J.K.")
40
+ attribute :initials_separator, :string, default: " "
41
+ # The first forename cites in full, the rest as initials
42
+ # ("Milena S."; document-history name forms)
43
+ attribute :subsequent_initials, :boolean, default: -> { false }
44
+ # The direct-order surname casing after the first creator:
45
+ # the name-date convention upcases
46
+ attribute :subsequent_surname_upcase, :boolean,
47
+ default: -> { true }
48
+ # At this many creators the creator list truncates to the
49
+ # first etalDisplay names with "et al." (0 disables)
50
+ attribute :etal_count, :integer, default: 0
51
+ attribute :etal_display, :integer, default: 0
52
+ # The in-text cite truncates separately from the creator
53
+ # list: at this many creators it shows the first
54
+ # inTextEtalDisplay names with "et al." (0 disables; IEEE
55
+ # cites 3+ as "Aluffi et al." while listing 6 in the
56
+ # bibliography)
57
+ attribute :in_text_etal_count, :integer, default: 0
58
+ attribute :in_text_etal_display, :integer, default: 1
59
+ # Editor lists carry the role marker ("(Eds.)")
60
+ attribute :editors_marked, :boolean, default: -> { true }
61
+
62
+ key_value do
63
+ map "initials", to: :initials
64
+ map "givenNameFirst", to: :given_name_first
65
+ map "rolePlacement", to: :role_placement
66
+ map "completenameUpcase", to: :completename_upcase
67
+ map "invertedAll", to: :inverted_all
68
+ map "listStyle", to: :list_style
69
+ map "orgUpcase", to: :org_upcase
70
+ map "creatorFallback", to: :creator_fallback
71
+ map "loneEditorMarked", to: :lone_editor_marked
72
+ map "initialsPeriod", to: :initials_period
73
+ map "surnameUpcase", to: :surname_upcase
74
+ map "initialsSeparator", to: :initials_separator
75
+ map "subsequentInitials", to: :subsequent_initials
76
+ map "subsequentSurnameUpcase", to: :subsequent_surname_upcase
77
+ map "etalCount", to: :etal_count
78
+ map "etalDisplay", to: :etal_display
79
+ map "inTextEtalCount", to: :in_text_etal_count
80
+ map "inTextEtalDisplay", to: :in_text_etal_display
81
+ map "editorsMarked", to: :editors_marked
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # CitationScheme: the citation system, its name form and its
8
+ # localized strings (relaton-models CitationScheme).
9
+ class Scheme < Lutaml::Model::Serializable
10
+ attribute :system, :string, default: "name-date"
11
+ attribute :name_form, NameForm, default: -> { NameForm.new }
12
+ attribute :locale, Locale, default: -> { Locale.new }
13
+ attribute :home_docid_type, :string, collection: true
14
+ attribute :default_kind, :string, default: ""
15
+
16
+ # The renderings' short cite takes the reference form with the
17
+ # first-biblio marker (the 1.x citetemplate short), not the
18
+ # citation template
19
+ attribute :short_from_reference, :boolean, default: -> { false }
20
+
21
+ key_value do
22
+ map "system", to: :system
23
+ map "nameForm", to: :name_form
24
+ map "locale", to: :locale
25
+ map "homeDocidType", to: :home_docid_type
26
+ map "defaultKind", to: :default_kind
27
+ map "shortFromReference", to: :short_from_reference
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # SortRule: one ordering key of a bibliographic index sort
8
+ # (relaton-models SortRule).
9
+ class SortRule < Lutaml::Model::Serializable
10
+ attribute :attribute, :string
11
+ attribute :order, :string
12
+
13
+ key_value do
14
+ map "attribute", to: :attribute
15
+ map "order", to: :order
16
+ end
17
+
18
+ def descending?
19
+ order == "descending"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # TemplateMap: citation and reference templates as data, plus the
8
+ # title emphasis markers and the first-creator name form. Slots
9
+ # name ISO 690 clause 7 data elements or name-form fields.
10
+ class TemplateMap < Lutaml::Model::Serializable
11
+ attribute :title_open, :string, default: "_"
12
+ attribute :title_close, :string, default: "_"
13
+ attribute :citation, :string, default: "{{surname}}, {{date}}"
14
+ attribute :reference, :string
15
+ attribute :name, :string, default: "{{surname}}, {{givenNames}}"
16
+ attribute :series, :string, default: ""
17
+ attribute :uri, :string, default: ""
18
+ attribute :creators, :string, default: ""
19
+ attribute :uri_short, :string, default: ""
20
+ # "first": cite only the first identifier (1.x
21
+ # authoritative_identifier|first styles)
22
+ attribute :identifier_mode, :string, default: "all"
23
+
24
+ key_value do
25
+ map "titleOpen", to: :title_open
26
+ map "titleClose", to: :title_close
27
+ map "citation", to: :citation
28
+ map "reference", to: :reference
29
+ map "name", to: :name
30
+ map "series", to: :series
31
+ map "uri", to: :uri
32
+ map "creators", to: :creators
33
+ map "uriShort", to: :uri_short
34
+ map "identifierMode", to: :identifier_mode
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ class Style
7
+ # TypeTemplate: the template variant for one bibliographic item
8
+ # type, selected by data lookup (relaton-models TypeTemplate).
9
+ class TypeTemplate < Lutaml::Model::Serializable
10
+ attribute :type, :string
11
+ attribute :template, :string
12
+ attribute :series, :string
13
+ attribute :title, :string
14
+ attribute :home, :boolean
15
+ attribute :fallbacks, :hash, default: -> { {} }
16
+ # The short-cite template for this kind; absent falls back to
17
+ # the reference template in short mode
18
+ attribute :short, :string
19
+ # Render the style's no_place label when the publisher has no
20
+ # place (the book-family templates of some flavors)
21
+ attribute :no_place, :boolean
22
+
23
+ key_value do
24
+ map "type", to: :type
25
+ map "template", to: :template
26
+ map "series", to: :series
27
+ map "title", to: :title
28
+ map "home", to: :home
29
+ map "fallbacks", to: :fallbacks
30
+ map "short", to: :short
31
+ map "noPlace", to: :no_place
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Relaton
6
+ module Render
7
+ module Iso690
8
+ # A citation style as an instance of the relaton-models CitationStyle
9
+ # model: the citation scheme (name form, localized strings), the
10
+ # citation and reference templates, and per-type template variants.
11
+ # All rendering knowledge is data; the engine holds no styles, no
12
+ # vocabularies, no localized strings.
13
+ class Style < Lutaml::Model::Serializable
14
+ autoload :NameForm, "relaton/render/iso690/style/name_form"
15
+ autoload :Locale, "relaton/render/iso690/style/locale"
16
+ autoload :Scheme, "relaton/render/iso690/style/scheme"
17
+ autoload :TemplateMap, "relaton/render/iso690/style/template_map"
18
+ autoload :SortRule, "relaton/render/iso690/style/sort_rule"
19
+ autoload :TypeTemplate, "relaton/render/iso690/style/type_template"
20
+
21
+ attribute :name, :string
22
+ # The citation-style family the pack belongs to (iso690 | lncs |
23
+ # chicago | ieee-sa | apa): selects the presentation-of-models
24
+ # conventions the templates address
25
+ attribute :family, :string, default: "iso690"
26
+ # A named pack this one deltas over; resolved at load with the
27
+ # documented merge semantics (#merge_delta)
28
+ attribute :extends, :string
29
+ # Engine capabilities the pack addresses; a resolver that lacks
30
+ # one fails loudly instead of mis-rendering
31
+ attribute :requires, :string, collection: true, default: []
32
+ # Pack-declared taxonomy: wire type -> kind (aliasing or
33
+ # fallthrough), consulted before the engine's Kinds table
34
+ attribute :types, :hash, default: -> { {} }
35
+ # Presentation-of-models selections: slot -> named engine rule
36
+ attribute :rules, :hash, default: -> { {} }
37
+ attribute :scheme, Scheme, default: -> { Scheme.new }
38
+ attribute :templates, TemplateMap, default: -> { TemplateMap.new }
39
+ attribute :per_type, TypeTemplate, collection: true, default: []
40
+ attribute :sort_key, SortRule, collection: true, default: []
41
+
42
+ key_value do
43
+ map "name", to: :name
44
+ map "family", to: :family
45
+ map "extends", to: :extends
46
+ map "requires", to: :requires
47
+ map "types", to: :types
48
+ map "rules", to: :rules
49
+ map "scheme", to: :scheme
50
+ map "templates", to: :templates
51
+ map "perType", to: :per_type
52
+ map "sortKey", to: :sort_key
53
+ end
54
+
55
+ # The index sort rules; creator then date when the style declares
56
+ # none (the name-and-date default).
57
+ def sort_keys
58
+ return sort_key unless sort_key.empty?
59
+
60
+ [SortRule.new(attribute: "creator"), SortRule.new(attribute: "date")]
61
+ end
62
+
63
+ # The ISO 690 kind of an item: the vocabulary's mapping, or
64
+ # the style's declared kind for types the vocabulary does not
65
+ # map (a flavor whose untyped items are not reports)
66
+ def kind_for(type)
67
+ declared_type = types[type.to_s]
68
+ unless declared_type.to_s.empty?
69
+ return Kinds.mapped?(declared_type) ? Kinds.kind_for(declared_type) :
70
+ declared_type
71
+ end
72
+
73
+ return Kinds.kind_for(type) if Kinds.mapped?(type)
74
+
75
+ declared = scheme.default_kind.to_s
76
+ declared.empty? ? Kinds.kind_for(type) : declared
77
+ end
78
+
79
+ # Per-type template selection is a data lookup; unmatched types
80
+ # fall back to the general reference template. When a kind has
81
+ # home-flagged variants, the split is resolved by whether the item
82
+ # carries the scheme's home document identifier.
83
+ def template_for(type, home: nil)
84
+ candidates = per_type.select { |t| t.type == type.to_s }
85
+ if candidates.size > 1 && !home.nil?
86
+ candidates = candidates.select { |t| (t.home || false) == home }
87
+ end
88
+ candidates.first&.template || templates.reference
89
+ end
90
+
91
+ def type_template_for(type)
92
+ per_type.find { |t| t.type == type.to_s }
93
+ end
94
+
95
+ # The home split applies to the short cite as it does to the
96
+ # reference template (a home standard's short cite is not an
97
+ # external standard's)
98
+ def short_template_for(type, home: nil)
99
+ candidates = per_type.select { |t| t.type == type.to_s }
100
+ if candidates.size > 1 && !home.nil?
101
+ candidates = candidates.select { |t| (t.home || false) == home }
102
+ end
103
+ candidates.filter_map(&:short).first
104
+ end
105
+
106
+ # The absent-slot replacement text declared for a type, when any
107
+ def fallback_for(type, slot)
108
+ type_template_for(type)&.fallbacks&.[](slot.to_s)
109
+ end
110
+
111
+ def title_form_for(type)
112
+ per_type.lazy.select { |t| t.type == type.to_s }
113
+ .find { |t| t.title && !t.title.empty? }&.title
114
+ end
115
+
116
+ # First-creator name form: the style's declared name template with
117
+ # the surname upcased, per the name-and-date convention.
118
+ def render_name(surname:, given:)
119
+ shown = scheme.name_form.surname_upcase ? surname.upcase : surname
120
+ Template.new(templates.name).evaluate(
121
+ "surname" => Template::Field[!shown.empty?, shown],
122
+ "givennames" => Template::Field[!given.empty?, given],
123
+ )
124
+ end
125
+
126
+ # The documented delta merge: perType entries merge per type
127
+ # key (a delta re-declaring `article` never drops the base's
128
+ # `monograph`); hash sections merge per key; scalars, templates
129
+ # and rule selections replace; sortKey and requires union.
130
+ # Deterministic and engine-portable.
131
+ def merge_delta(base)
132
+ merged_per_type = base.per_type.dup
133
+ per_type.each do |delta|
134
+ index = merged_per_type.find_index do |b|
135
+ b.type == delta.type && (b.home || false) == (delta.home || false)
136
+ end
137
+ index ? merged_per_type[index] = delta : merged_per_type << delta
138
+ end
139
+ self.per_type = merged_per_type
140
+ self.types = base.types.merge(types)
141
+ self.rules = base.rules.merge(rules)
142
+ merge_scheme(base)
143
+ merge_templates(base)
144
+ self.sort_key = (base.sort_key + sort_key).uniq
145
+ self.requires = (base.requires + requires).uniq
146
+ self
147
+ end
148
+
149
+ private
150
+
151
+ def merge_scheme(base)
152
+ fresh = NameForm.new
153
+ NameForm.attributes.each_key do |attr|
154
+ val = scheme.name_form.send(attr)
155
+ next unless val == fresh.send(attr)
156
+
157
+ base_val = base.scheme.name_form.send(attr)
158
+ scheme.name_form.send("#{attr}=", base_val) if
159
+ base_val != fresh.send(attr)
160
+ end
161
+
162
+ scheme.system = base.scheme.system if scheme.system.to_s.empty?
163
+ scheme.short_from_reference = base.scheme.short_from_reference unless
164
+ scheme.short_from_reference
165
+ scheme.home_docid_type = base.scheme.home_docid_type if
166
+ Array(scheme.home_docid_type).empty?
167
+ scheme.default_kind = base.scheme.default_kind if
168
+ scheme.default_kind.to_s.empty?
169
+
170
+ locale = scheme.locale
171
+ base_locale = base.scheme.locale
172
+ locale.conj ||= base_locale.conj
173
+ locale.others ||= base_locale.others
174
+ locale.no_date ||= base_locale.no_date
175
+ locale.no_author ||= base_locale.no_author
176
+ locale.in_str ||= base_locale.in_str
177
+ locale.at ||= base_locale.at
178
+ locale.available_at ||= base_locale.available_at
179
+ locale.labels.merge!(base_locale.label_map) { |_k, _b, d| d }
180
+ locale.punct = (base_locale.punct || {}).merge(locale.punct || {})
181
+ end
182
+
183
+ def merge_templates(base)
184
+ fresh = TemplateMap.new
185
+ TemplateMap.attributes.each_key do |attr|
186
+ val = templates.send(attr)
187
+ base_val = base.templates.send(attr)
188
+ next unless val == fresh.send(attr) && base_val != fresh.send(attr)
189
+
190
+ templates.send("#{attr}=", base_val)
191
+ end
192
+ end
193
+
194
+ class << self
195
+ def load(name_or_path, seen = [])
196
+ path = style_path(name_or_path) or
197
+ raise ArgumentError, "unknown style #{name_or_path}"
198
+ style = from_yaml(File.read(path))
199
+ return style if style.extends.to_s.empty?
200
+
201
+ style.extends.split(/\s+/).each do |base_name|
202
+ raise ArgumentError,
203
+ "circular extends #{base_name}" if seen.include?(base_name)
204
+
205
+ style.merge_delta(load(base_name, seen + [base_name]))
206
+ end
207
+ style
208
+ end
209
+
210
+ private
211
+
212
+ def style_path(name)
213
+ file = File.join(__dir__, "styles", "#{name}.yml")
214
+ return file if File.file?(file)
215
+
216
+ name if name.is_a?(String) && File.file?(name)
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end