relaton-render 3.0.0.pre.alpha.21 → 3.0.0.pre.alpha.23

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfc55ca6da22673e6173486336211c48510588218d75e08672e8b3cfc09308cd
4
- data.tar.gz: 0a338c96b9ec80bbd6b345c86d6fa15a9b14810db27729104f1386391f46de69
3
+ metadata.gz: 84f33badbec6cb9d4d80c763d0538b9a3bde40dea703623568cb52a4758134bf
4
+ data.tar.gz: b6d9d4b6317b917472350aa45c7c7056b4eb8f68337daea43a97a3603cbf8517
5
5
  SHA512:
6
- metadata.gz: 4d172ba4dd2697389d3372a37eaf460d7673ab7a2ba693e8365f8777f241c8dc19df5b2dc9082563aaeeb08866634318dccfbed54d61a378b341ee0c7b88e954
7
- data.tar.gz: c0258407ac4ca2672164299ffe50b4ea671498dced9f653b06eb818e448e824e405b77e321a4e756f17710aba74a7f2892d69dfea3d7e6e216f84e23c6e6b190
6
+ metadata.gz: dd0961f370beaf6e0bc3b5233d71aee7d47773b4443a956de18278228356f164f90c6e62a9da2bb19019f1cd4076675fdc85fa9e56b1f47395d0d134912540dc
7
+ data.tar.gz: 4331fc6e0974c1975b8340e374f23522f7d252aa6dfb3b65d37afa8da919128c78f5b7589115c06c25cf74dfac0649546177b4f8494dd969cb6291e486f8401c
@@ -79,13 +79,15 @@ module Relaton
79
79
  next
80
80
  end
81
81
  m[item.id] = {
82
- id: item.id, ord: i,
83
82
  author: renderer.in_text_author(item),
84
- formattedref: terminate_reference(ref, item),
83
+ date: renderer.disambiguated_date(item,
84
+ disambiguator:
85
+ disambiguators[item.id]),
85
86
  citation: citation_renderings(item, ref, renderer,
86
87
  type: type,
87
88
  disambiguator:
88
89
  disambiguators[item.id]),
90
+ formattedref: terminate_reference(ref, item),
89
91
  }
90
92
  end
91
93
  end
@@ -93,7 +95,7 @@ module Relaton
93
95
  # isodoc's styled citations consume these keys: the default is the
94
96
  # authoritative identifier; the short cite is the style's citation,
95
97
  # falling back to the reference rendering (the 1.x short-cite)
96
- FIRST_DELIM = "<span class='fmt-first-biblio-delim'/>"
98
+ FIRST_DELIM = '<span class="fmt-first-biblio-delim"/>'
97
99
 
98
100
  # A creator list alone (flavors' document-history name forms)
99
101
  def creator_names(item)
@@ -141,22 +143,33 @@ module Relaton
141
143
  # names the key)
142
144
  def citation_renderings(item, ref, renderer, type: "author-date",
143
145
  disambiguator: nil)
144
- short = renderer.citation(item)
145
- short = if short.empty?
146
+ # A style declaring shortFromReference takes the reference form
147
+ # with the first-biblio marker (the 1.x citetemplate short)
148
+ short = if renderer.short_from_reference?
146
149
  renderer.render_short(item, FIRST_DELIM)
147
150
  else
148
- short
151
+ short = renderer.citation(item)
152
+ short.empty? ? renderer.render_short(item, FIRST_DELIM) :
153
+ short
149
154
  end
150
155
 
156
+ title = renderer.title_citation(item)
157
+
151
158
  {
152
159
  full: ref,
153
160
  default: item.docidentifier.first&.content.to_s,
154
161
  short: short,
155
- author_date: renderer.author_date_citation(
162
+ author_date: renderer.citation(item,
163
+ disambiguator: disambiguator),
164
+ author_date_br: renderer.bracketed_date_citation(
156
165
  item, disambiguator: disambiguator,
157
166
  ),
158
167
  author: renderer.in_text_author(item),
159
- title: renderer.title_citation(item),
168
+ date: renderer.disambiguated_date(item),
169
+ # reference_tag: no rendering — isodoc falls back to the
170
+ # biblio-tag anchor (the 1.x nil)
171
+ title: title,
172
+ title_reference_tag: title,
160
173
  }
161
174
  end
162
175
 
@@ -232,7 +245,9 @@ module Relaton
232
245
  end
233
246
  out.empty? or break
234
247
  end
235
- out.map { |x| x.text.strip }
248
+ # esc-wrapped: isodoc's l10n skips the tags, leaving a standard
249
+ # identifier's punctuation verbatim (the 1.x Parse behaviour)
250
+ out.map { |x| "<esc>#{x.text.strip}</esc>" }
236
251
  end
237
252
 
238
253
  def facade_id_type(id)
@@ -256,7 +271,8 @@ module Relaton
256
271
  # and every flavor still pass string keys.
257
272
  def deep_symbolize(value)
258
273
  case value
259
- when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
274
+ # isodoc i18n hashes carry nil keys (an unlabeled punct slot)
275
+ when Hash then value.to_h { |k, v| [k&.to_sym, deep_symbolize(v)] }
260
276
  when Array then value.map { |v| deep_symbolize(v) }
261
277
  else value
262
278
  end
@@ -118,9 +118,12 @@ module Relaton
118
118
  end
119
119
 
120
120
  def join_names(names)
121
- # A style with no join word renders a plain serial list
121
+ # A style with no join word renders a plain serial list,
122
+ # separated per the locale (the comma, or the CJK 、)
122
123
  if @i18n.label("and").empty? && names.size > 1
123
- return names.join(", ")
124
+ sep = @i18n.label("list_separator")
125
+ sep = ", " if sep == "list_separator"
126
+ return names.join(sep)
124
127
  end
125
128
 
126
129
  oxford = @i18n.label("oxford_comma")
@@ -34,12 +34,12 @@ module Relaton
34
34
  # from the next element, which follows directly in the
35
35
  # template; a lone editor is unmarked unless declared
36
36
  if editors? && marked?
37
- "#{period(joined)} (#{role_word}) "
37
+ "#{period(joined)}#{role_space}#{role_parenthesis} "
38
38
  else
39
39
  "#{period(joined)} "
40
40
  end
41
41
  else
42
- marker = editors? && marked? ? " (#{role_word})" : ''
42
+ marker = editors? && marked? ? "#{role_space}#{role_parenthesis}" : ''
43
43
  "#{joined}#{marker}"
44
44
  end
45
45
  end
@@ -48,10 +48,29 @@ module Relaton
48
48
  # plain join, without the wrap and sentence period (1.x
49
49
  # short-cite templates carried no wrap)
50
50
  def plain_render
51
- marker = editors? && creators.size > 1 ? " (#{role_word})" : ''
51
+ marker = editors? && marked? ? "#{role_space}#{role_parenthesis}" : ''
52
52
  "#{join(names)}#{marker}"
53
53
  end
54
54
 
55
+ # The editor role marker's parentheses follow the locale
56
+ # (ASCII for Latin scripts, fullwidth for CJK)
57
+ def role_parenthesis
58
+ open = @i18n.punct_fetch("open-paren", "(")
59
+ close = @i18n.punct_fetch("close-paren", ")")
60
+ "#{open}#{role_word}#{close}"
61
+ end
62
+
63
+ # CJK cites the role marker tight against the name list
64
+ def role_space
65
+ @i18n.punct_fetch("marker-space", " ")
66
+ end
67
+
68
+ # The template-level role slot ("{{role}}"): the marker with
69
+ # its locale spacing, when the creators are all editors
70
+ def role_marker
71
+ editors? ? "#{role_space}#{role_parenthesis}" : ""
72
+ end
73
+
55
74
  def role_placement
56
75
  @style.scheme.name_form.role_placement || "glue"
57
76
  end
@@ -14,10 +14,39 @@ module Relaton
14
14
  def render
15
15
  return "#{@i18n.label('version')} #{raw}" unless word_edition?
16
16
 
17
+ # The worded forms first ("First edition", "第N版" via the
18
+ # ordinal template), the cardinal last
19
+ ordinal = locale_template("edition_ordinal", number)
20
+ return ordinal if ordinal
21
+
17
22
  word = @i18n.label("edition_#{number}")
18
23
  return word unless word == "edition_#{number}"
19
24
 
20
- "#{ordinalize(number)} #{@i18n.label('edition')}"
25
+ cardinal_form || "#{ordinalize(number)} #{@i18n.label('edition')}"
26
+ end
27
+
28
+ # The plain cardinal form ("edition 7")
29
+ def cardinal_form
30
+ locale_template("edition_cardinal", number) ||
31
+ locale_template("version_cardinal", number)
32
+ end
33
+
34
+ private
35
+
36
+ # A locale template carrying {{ var1 }} (optionally through
37
+ # the ordinal_word filter) populated with the edition number;
38
+ # a bare label (no placeholder) is not one
39
+ def locale_template(key, value)
40
+ raw = @i18n.label(key)
41
+ return nil if raw == key || raw !~ /\{{/
42
+
43
+ if raw.include?("ordinal_word")
44
+ word = @i18n.label("ordinal_word_#{value}")
45
+ word = ordinalize(value) if word == "ordinal_word_#{value}"
46
+ raw.gsub(/\{\{\s*var1[^}]*\}\}/, word)
47
+ else
48
+ raw.gsub("{{ var1 }}", value.to_s)
49
+ end
21
50
  end
22
51
 
23
52
  private
@@ -12,17 +12,34 @@ module Relaton
12
12
 
13
13
  def render
14
14
  groups = [numeration, page_range].reject { |g| g.empty? }
15
- groups.join(", ")
15
+ groups.join(group_sep)
16
16
  end
17
17
 
18
18
  private
19
19
 
20
+ # An extent label is a template over its value: "vol. %" in
21
+ # the Latin pack, "巻%" / "%頁" in the CJK packs
22
+ def unit(type, value)
23
+ label = @i18n.label(type)
24
+ return "#{label} #{value}" unless label.include?("%")
25
+
26
+ label.sub("%", value)
27
+ end
28
+
29
+ def cjk?(label)
30
+ label.match?(/\p{Han}|\p{Hiragana}|\p{Katakana}/)
31
+ end
32
+
33
+ def group_sep
34
+ cjk?(@i18n.label("page")) ? "、 " : ", "
35
+ end
36
+
20
37
  def numeration
21
38
  %w[volume issue].filter_map do |type|
22
39
  loc = localities.find { |l| l.type == type }
23
40
  next if loc.nil? || loc.reference_from.to_s.empty?
24
41
 
25
- "#{@i18n.label(type)} #{loc.reference_from}"
42
+ unit(type, loc.reference_from)
26
43
  end.join(" ")
27
44
  end
28
45
 
@@ -31,10 +48,10 @@ module Relaton
31
48
  from = loc.reference_from.to_s
32
49
  to = loc.reference_to.to_s
33
50
  if to.empty? || to == from
34
- "#{@i18n.label('page')} #{from}"
51
+ unit("page", from)
35
52
  else
36
- "#{@i18n.label('pages')} " \
37
- "#{from}#{@i18n.label('date_range')}#{to}"
53
+ range = "#{from}#{@i18n.label('date_range')}#{to}"
54
+ unit("pages", range)
38
55
  end
39
56
  end
40
57
 
@@ -17,17 +17,24 @@ module Relaton
17
17
  return no_place_production_publisher_first if places.empty? &&
18
18
  no_place_declared?
19
19
 
20
- return [publishers.join("; "), places.join("; ")]
20
+ return [publishers.join("; "), places.map { |p| cjk_place(p) }]
21
21
  .reject(&:empty?).join(production_sep)
22
22
  end
23
23
 
24
24
  return no_place_production if places.empty? && no_place_declared?
25
25
 
26
- [places.join("; "), publishers.join("; ")]
26
+ [places.map { |p| cjk_place(p) }, publishers.join("; ")]
27
27
  .reject(&:empty?).join(production_sep)
28
28
  end
29
29
 
30
- def publisher_first?
30
+ # A CJK locale's place names carry the CJK-Latin separator
31
+ # ("Cambridge、UK" over "Cambridge, UK")
32
+ def cjk_place(place)
33
+ sep = @i18n.punct_fetch("cjk-latin-separator", "")
34
+ sep.empty? ? place : place.gsub(", ", sep)
35
+ end
36
+
37
+ def publisher_first?
31
38
  @i18n.punct_fetch("production_order", "") == "publisher_first"
32
39
  end
33
40
 
@@ -50,7 +57,8 @@ module Relaton
50
57
  return "" if label.empty? || label == "no_place"
51
58
 
52
59
  pubs = publishers.join("; ")
53
- pubs.empty? ? "#{label}:" : "#{label}: #{pubs}"
60
+ pubs.empty? ? "#{label}#{production_sep}".rstrip :
61
+ "#{label}#{production_sep}#{pubs}"
54
62
  end
55
63
 
56
64
  def no_place_declared?
@@ -25,7 +25,11 @@ module Relaton
25
25
  if number.empty?
26
26
  base
27
27
  else
28
- "#{base}, #{@i18n.label('series_no')} #{number}"
28
+ # an empty series_no label joins on the space alone (the
29
+ # CJK form: "Series 472")
30
+ label = @i18n.label("series_no")
31
+ label.empty? ? "#{base} #{number}" :
32
+ "#{base}, #{label} #{number}"
29
33
  end
30
34
  end
31
35
 
@@ -28,7 +28,11 @@ module Relaton
28
28
 
29
29
  type = value.is_a?(Hash) ? value[:type] : value.type
30
30
  unit = type && @i18n.label("size_#{type}")
31
- unit.nil? || unit == "size_#{type}" ? content : "#{content} #{unit}"
31
+ return content if unit.nil? || unit == "size_#{type}"
32
+
33
+ # The value/unit glue is locale-declared (CJK cites tight,
34
+ # "巻1" over "vol. 1")
35
+ "#{unit}#{@i18n.punct_fetch('size-join', ' ')}#{content}"
32
36
  end
33
37
  end
34
38
  end
@@ -30,6 +30,7 @@ module Relaton
30
30
  key = slot.to_s.delete("_").downcase
31
31
  table[key] = element_field(slot)
32
32
  end
33
+ table["role"] = role_field
33
34
  table["dategroup"] = dategroup_field
34
35
  table["disambiguateddate"] = disambiguated_date_field(table["date"])
35
36
  unless table["dategroup"].present?
@@ -38,6 +39,20 @@ module Relaton
38
39
  table.merge(name_fields)
39
40
  end
40
41
 
42
+ # The creators' editor-role marker ("(eds.)", "(編)"), as a
43
+ # template slot: a creator list that is not all editors cites no
44
+ # role
45
+ def role_field
46
+ creator = Elements.build(:creator, @model, style: @style,
47
+ i18n: @i18n, elements: @elements)
48
+ return Template::Field[false, ""] unless
49
+ creator&.present? && creator.respond_to?(:role_marker)
50
+
51
+ marker = creator.role_marker
52
+ marker.empty? ? Template::Field[false, ""] :
53
+ Template::Field[true, marker]
54
+ end
55
+
41
56
  # The date carrying the batch disambiguation suffix ("2022a"):
42
57
  # identity when the caller passes none
43
58
  def disambiguated_date_field(date)
@@ -1,12 +1,23 @@
1
1
  lang: en
2
2
  labels:
3
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"
4
14
  size_volume: "vol."
5
15
  size_page: "pp."
6
- page: "p."
7
- pages: "pp."
8
- volume: "vol."
9
- issue: "no."
16
+ page: "p. %"
17
+ pages: "pp. %"
18
+ volume: "vol. %"
19
+ issue: "no. %"
20
+ no_place: "n.p."
10
21
  version: "Version"
11
22
  medium_website: "[website]"
12
23
  viewed: "viewed"
@@ -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: ")"
@@ -43,6 +43,7 @@ module Relaton
43
43
  known = %w[
44
44
  edition series_no report_no available_from in and oxford_comma
45
45
  ed eds date_range others no_date no_author at
46
+ edition_ordinal edition_cardinal version_cardinal
46
47
  ]
47
48
  hash.each do |key, value|
48
49
  labels[key.to_s] = value if known.include?(key.to_s) && !value.nil?
@@ -64,7 +64,8 @@ module Relaton
64
64
  # The short cite: the reference with the first-biblio marker
65
65
  def render_short(model, delim)
66
66
  kind = @style.kind_for(model.type)
67
- template = @style.type_template_for(kind)&.short ||
67
+ template = @style.short_template_for(kind,
68
+ home: home_docid?(model)) ||
68
69
  @style.template_for(kind, home: home_docid?(model))
69
70
  Template.new(template).evaluate_short(
70
71
  Fields.new(model, style: @style, i18n: @i18n, short: true,
@@ -92,6 +93,32 @@ module Relaton
92
93
  .reject(&:empty?).join(" ")
93
94
  end
94
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
+
95
122
  # The title citation ("{{ title }}" in the 1.x citetemplate):
96
123
  # the title element's plain form, no style markers
97
124
  def title_citation(model)
@@ -13,12 +13,18 @@ module Relaton
13
13
  attribute :home_docid_type, :string, collection: true
14
14
  attribute :default_kind, :string, default: ""
15
15
 
16
- key_value do
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
17
22
  map "system", to: :system
18
23
  map "nameForm", to: :name_form
19
24
  map "locale", to: :locale
20
25
  map "homeDocidType", to: :home_docid_type
21
26
  map "defaultKind", to: :default_kind
27
+ map "shortFromReference", to: :short_from_reference
22
28
  end
23
29
  end
24
30
  end
@@ -66,6 +66,17 @@ module Relaton
66
66
  per_type.find { |t| t.type == type.to_s }
67
67
  end
68
68
 
69
+ # The home split applies to the short cite as it does to the
70
+ # reference template (a home standard's short cite is not an
71
+ # external standard's)
72
+ def short_template_for(type, home: nil)
73
+ candidates = per_type.select { |t| t.type == type.to_s }
74
+ if candidates.size > 1 && !home.nil?
75
+ candidates = candidates.select { |t| (t.home || false) == home }
76
+ end
77
+ candidates.filter_map(&:short).first
78
+ end
79
+
69
80
  # The absent-slot replacement text declared for a type, when any
70
81
  def fallback_for(type, slot)
71
82
  type_template_for(type)&.fallbacks&.[](slot.to_s)
@@ -30,8 +30,18 @@ module Relaton
30
30
  present = present_indices(fields)
31
31
  return "" if present.empty?
32
32
 
33
+ # The split marker follows the leading run of present slots
34
+ # (the 1.x first SEGMENT: "{{creator}}{{role}}" splits after
35
+ # the role), before the first literal that follows it
36
+ lead_slots = []
37
+ @chunks.each_with_index do |(kind, _value), i|
38
+ break if kind != :slot || !present.include?(i)
39
+
40
+ lead_slots << i
41
+ end
42
+ split_at = lead_slots.last ? lead_slots.last + 1 : nil
43
+
33
44
  out = +""
34
- first_seen = false
35
45
  @chunks.each_with_index do |(kind, value), i|
36
46
  text =
37
47
  case kind
@@ -39,18 +49,20 @@ module Relaton
39
49
  field = fields[normalise(value)]
40
50
  next unless field&.present?
41
51
 
42
- first_seen ? field.text : "#{field.text}#{delim} "
52
+ field.text
43
53
  else
44
- # the split marker precedes the component's trailing
45
- # punctuation (1.x ret[0] += delim, then join), with
46
- # the join's separating space
47
54
  next unless bridged?(i, present)
48
55
 
49
56
  value
50
57
  end
51
- first_seen = true if @chunks[i].first == :slot
58
+ if split_at == i
59
+ out << fieldless(lead_slots, fields) if false
60
+ out << delim.to_s
61
+ split_at = nil
62
+ end
52
63
  out << text unless text.nil?
53
64
  end
65
+ out << delim.to_s if split_at == @chunks.length
54
66
  terminate(out).rstrip
55
67
  end
56
68
 
@@ -64,10 +76,44 @@ module Relaton
64
76
  return "" if present.empty?
65
77
 
66
78
  out = +"".dup
79
+ # Parenthetical groups balance on the template's literal
80
+ # parens alone: data text (a series run "(N.S.)", a host
81
+ # creator's closing paren) carries parens of its own that
82
+ # are no part of the template's group structure
83
+ paren_balance = 0
67
84
  @chunks.each_with_index do |(kind, value), i|
68
85
  if kind == :slot
69
86
  field = fields[normalise(value)]
70
87
  out << field.text if field&.present?
88
+ elsif value.to_s.match?(/\A[))]/)
89
+ # Symmetric close: with no pending open the close-paren
90
+ # drops, and any trailing punctuation bridges normally
91
+ closes = value.to_s[/\A[))]+/]
92
+ emitted = [paren_balance, closes.size].min
93
+ if emitted.positive?
94
+ out << closes[0, emitted] +
95
+ value.to_s[closes.size..].to_s
96
+ paren_balance -= emitted
97
+ else
98
+ rest = value.to_s.sub(/\A[))]+/, "")
99
+ # the trailing separator of a dropped group bridges as
100
+ # though the group were never there
101
+ if !rest.empty? && present.any? { |j| j > i } &&
102
+ group_gap?(i, present)
103
+ out << rest
104
+ end
105
+ end
106
+ elsif value.to_s.match?(/[((]\z/)
107
+ # A literal opening a parenthetical group ("({{series}})",
108
+ # "。({{series}})") attaches its paren to the slot it
109
+ # opens — the pair drops with an absent element, while
110
+ # any leading punctuation bridges normally
111
+ lead, paren = value.to_s.match(/\A(.*)([((])\z/).captures
112
+ out << lead if bridged_text?(i, lead, present)
113
+ if present.include?(i + 1)
114
+ out << paren
115
+ paren_balance += 1
116
+ end
71
117
  elsif bridged?(i, present) ||
72
118
  (value.include?("<") && present.any? { |j| j < i })
73
119
  # markup attaches to the element it closes
@@ -102,6 +148,30 @@ module Relaton
102
148
  end
103
149
  end
104
150
 
151
+ # The texts between this close and the last present slot are
152
+ # all parenthetical-group parts (each dropped or partial)
153
+ def group_gap?(index, present)
154
+ left = present.select { |i| i < index }.max
155
+ @chunks[(left + 1)...index].all? do |(_, text)|
156
+ text.to_s.match?(/\A[))]|[((]\z/)
157
+ end
158
+ end
159
+
160
+ # The leading punctuation of a paren-opening literal bridges
161
+ # per the segment-join rule (a bare paren has no lead)
162
+ def bridged_text?(index, lead, present)
163
+ return true if lead.empty?
164
+
165
+ first_slot = @chunks.index { |(kind, _)| kind == :slot }
166
+ return present.include?(first_slot) if index < first_slot
167
+
168
+ left = present.select { |i| i < index }.max
169
+ left && present.any? { |i| i > index } &&
170
+ !@chunks[(left + 1)...index].any? do |(kind, _)|
171
+ kind == :text
172
+ end
173
+ end
174
+
105
175
  # A literal is the trailing separator of the element that
106
176
  # precedes it: it renders when that element renders and a
107
177
  # later element renders, and it is the FIRST literal of its
@@ -124,9 +194,14 @@ module Relaton
124
194
  end
125
195
 
126
196
  # A template's paired literal ("({{production}})") closes even
127
- # when nothing follows the present element it wraps
197
+ # when nothing follows the present element it wraps. Fullwidth
198
+ # parens (CJK) pair the same way
128
199
  def close_pending_paren?(out, value)
129
- value.start_with?(")") && out.count("(") > out.count(")")
200
+ return false unless value.start_with?(")", "\uFF09")
201
+
202
+ opens = out.count("(") + out.count("\uFF08")
203
+ closes = out.count(")") + out.count("\uFF09")
204
+ opens > closes
130
205
  end
131
206
 
132
207
  def terminate(body)
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Render
3
- VERSION = "3.0.0.pre.alpha.21".freeze
3
+ VERSION = "3.0.0.pre.alpha.23".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-render
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.pre.alpha.21
4
+ version: 3.0.0.pre.alpha.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-06 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: htmlentities
@@ -143,6 +143,7 @@ files:
143
143
  - lib/relaton/render/iso690/i18n.rb
144
144
  - lib/relaton/render/iso690/i18n/en.yml
145
145
  - lib/relaton/render/iso690/i18n/fr.yml
146
+ - lib/relaton/render/iso690/i18n/ja.yml
146
147
  - lib/relaton/render/iso690/identifier_kinds.rb
147
148
  - lib/relaton/render/iso690/index_renderer.rb
148
149
  - lib/relaton/render/iso690/kinds.rb