isodoc 3.7.1 → 3.7.2

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: a85471023c45913439b8f282b641e3ef150cc8ae85a98abe0175308c4a0f66c7
4
- data.tar.gz: de812d85b19a63560c65cf1920b25bed1f0bf53a1c94cd63cf45414c37d8b816
3
+ metadata.gz: 3f336d204ef62fe18fa00d602cd7281ef876b14cc63c216e718eb8bf60be0f5d
4
+ data.tar.gz: 354606fcf812e4d4378a36dac7aa771e875b947fa21ade182faec0b52cff63f9
5
5
  SHA512:
6
- metadata.gz: b3c04391ab690ff67d9c2841158feb4c92f8f4f1e35fc62e6bf61daf64abd8fcec08193af1bdbff30d2bc4feec1994d75c1920c461e4ca0e38720ff8cebe1cf6
7
- data.tar.gz: 77377cce8adc055116f91c972c6204c9aecd15a95df79c66b54ab07e78d7a2c0a1efa2e60262a8a1542b1f3c623da53b7f83631ce6a222e2107602ac6370039f
6
+ metadata.gz: e29dd8f0a3e9181226c4df3dccc334497a313052b841671bfbf9b342fe577f4778578c3018eee82409a664f7a11cc937f7ed539dec6d74d2d2d8485a9d3f64cf
7
+ data.tar.gz: 2e1de040cfb8293321f989e31250f31b7b3c8a33167eb0846e34e08ae40d329f9e69342d6010b05b23aae615260457b3023d4f02baf084af5c6429dbe94e115d
data/.rubocop.yml CHANGED
@@ -2,6 +2,15 @@
2
2
  # See https://github.com/metanorma/cimas
3
3
  inherit_from:
4
4
  - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
5
+ # .rubocop_todo.yml MUST be the last entry. inherit_from is last-wins:
6
+ # if listed before oss-guides, the shared config's stricter Metrics/
7
+ # (MethodLength, BlockLength, etc.) rules override the todo's per-file
8
+ # grandfathering, and the todo becomes inert on those cops. Empirically
9
+ # verified on suma 2026-07-06: with todo listed first, 34 Metrics/* offenses
10
+ # remained; moved last, cleared. Every gem in the metanorma-org fleet
11
+ # already ships a `.rubocop_todo.yml` on its live tree (audited 2026-07-06,
12
+ # 54/54 have it), so this reference is safe to emit unconditionally.
13
+ - .rubocop_todo.yml
5
14
 
6
15
  # Rubocop plugins enabled centrally so every metanorma-org gem picks them up
7
16
  # on cimas sync — best practice belongs at the shared-template layer, not
@@ -130,6 +130,10 @@ dl dd {
130
130
  grid-column-start: 2;
131
131
  }
132
132
 
133
+ dl:has(dt .stem) {
134
+ grid-template-columns: fit-content(8em) auto;
135
+ }
136
+
133
137
  b, strong {
134
138
  font-weight: bold;
135
139
  }
@@ -1,8 +1,19 @@
1
1
  module IsoDoc
2
2
  module Function
3
3
  module Section
4
+ # Optional per-caption-type delimiter override from i18n: a
5
+ # +<type>_delimiter+ key, falling back to a shared +caption_delimiter+
6
+ # key, falling back to the hardcoded default. Uses +@i18n.get+ (hash
7
+ # lookup, nil for absent keys) rather than +@i18n.<key>+ (which is only
8
+ # defined for present keys). The keys are absent from the base i18n YAML,
9
+ # so default behaviour is unchanged unless a flavour or document sets them.
10
+ def i18n_delim(type, default)
11
+ @i18n&.get&.dig("#{type}_delimiter") ||
12
+ @i18n&.get&.dig("caption_delimiter") || default
13
+ end
14
+
4
15
  def clausedelim
5
- "."
16
+ i18n_delim("clause", ".")
6
17
  end
7
18
 
8
19
  def clausedelimspace(_node, out)
@@ -173,7 +173,11 @@ module IsoDoc
173
173
  true
174
174
  end
175
175
 
176
- def tr_parse(node, out, ord, totalrows, header)
176
+ # Defaults cover the bare-tr dispatch in ToWordHtml#parse, which had
177
+ # called this with two arguments (unsatisfiable) since 2018: a tr
178
+ # outside tbody/thead context renders as an unbordered plain row
179
+ # instead of crashing (metanorma-pdfa#84)
180
+ def tr_parse(node, out, ord = 0, totalrows = 1, header = false)
177
181
  out.tr(**attr_code(style: node["style"])) do |r|
178
182
  node.elements.each do |td|
179
183
  attrs = make_tr_attr(td, ord, totalrows - 1, header,
data/lib/isodoc/init.rb CHANGED
@@ -35,6 +35,9 @@ module IsoDoc
35
35
  toc_init(xml)
36
36
  @details_open = xml.at(ns("//metanorma-extension/presentation-metadata/" \
37
37
  "html-details-open"))&.text != "false"
38
+ @index_fold_case =
39
+ xml.at(ns("//metanorma-extension/presentation-metadata/" \
40
+ "index-case-sensitive"))&.text != "true"
38
41
  end
39
42
 
40
43
  def doctype_init(docxml)
@@ -91,7 +91,7 @@ module IsoDoc
91
91
  end
92
92
 
93
93
  def annex_delim(_elem)
94
- "<br/><br/>"
94
+ i18n_delim("annex", "<br/><br/>")
95
95
  end
96
96
  end
97
97
  end
@@ -46,7 +46,8 @@ module IsoDoc
46
46
  n = @xrefs.get[elem["id"]] || @xrefs.get[elem["original-id"]]
47
47
  lbl = labelled_autonum(@i18n.example, elem["id"] || elem["original-id"],
48
48
  n&.dig(:label))
49
- prefix_name(elem, { caption: block_delim }, lbl, "name")
49
+ prefix_name(elem, { caption: i18n_delim("example", block_delim) }, lbl,
50
+ "name")
50
51
  end
51
52
 
52
53
  def note(docxml)
@@ -54,7 +55,7 @@ module IsoDoc
54
55
  end
55
56
 
56
57
  def note_delim(_elem)
57
- "<tab/>"
58
+ i18n_delim("note", "<tab/>")
58
59
  end
59
60
 
60
61
  def note1(elem)
@@ -88,7 +89,8 @@ module IsoDoc
88
89
 
89
90
  def admonition_options(elem)
90
91
  options = { label: admonition_delim(elem) }
91
- elem["type"] == "box" and options = { caption: block_delim }
92
+ elem["type"] == "box" and
93
+ options = { caption: i18n_delim("admonition", block_delim) }
92
94
  options
93
95
  end
94
96
 
@@ -119,7 +121,7 @@ module IsoDoc
119
121
 
120
122
  def admonition_delim(elem)
121
123
  if elem["type"] == "box"
122
- block_delim
124
+ i18n_delim("admonition", block_delim)
123
125
  else
124
126
  ""
125
127
  end
@@ -157,7 +159,7 @@ module IsoDoc
157
159
  end
158
160
 
159
161
  def table_delim
160
- block_delim
162
+ i18n_delim("table", block_delim)
161
163
  end
162
164
 
163
165
  def table_long_strings_cleanup(docxml)
@@ -21,6 +21,9 @@ module IsoDoc
21
21
 
22
22
  def concept_render(node, defaults)
23
23
  opts, render, ref, ret = concept_render_init(node, defaults)
24
+ # generator-emitted term-resolution failure reports
25
+ # (metanorma-model-iso#144) render in boldface
26
+ ret&.xpath(ns(".//errormsg"))&.each { |e| e.name = "strong" }
24
27
  ret&.at(ns("./refterm"))&.remove
25
28
  ref && opts[:ref] != "false" and render&.next = " "
26
29
  concept1_linkmention(ref, render, opts)
@@ -8,12 +8,17 @@ module IsoDoc
8
8
  def related1(node)
9
9
  p, ref, orig = related1_prep(node)
10
10
  label = @i18n.relatedterms[orig["type"]].upcase
11
+ err = node.at(ns("./errormsg"))
11
12
  ret = if p && ref
12
13
  if p.text == ref.text
13
14
  "<em>#{Common::to_xml(ref)}</em>"
14
15
  else
15
16
  "<em>#{to_xml(p)}</em> (#{Common::to_xml(ref)})"
16
17
  end
18
+ elsif err
19
+ # generator-emitted term-resolution failure reports
20
+ # (metanorma-model-iso#144) render in boldface
21
+ "<strong>#{to_xml(err.children)}</strong>"
17
22
  else "<strong>**RELATED TERM NOT FOUND**</strong>"
18
23
  end
19
24
  node.children = (l10n("<p><strong>#{label}:</strong> #{ret}</p>"))
@@ -54,12 +54,11 @@ module IsoDoc
54
54
  end
55
55
 
56
56
  def subfigure_delim
57
- ""
58
- # "<span class='fmt-label-delim'>)</span>"
57
+ i18n_delim("subfigure", "")
59
58
  end
60
59
 
61
60
  def figure_delim(_elem)
62
- block_delim
61
+ i18n_delim("figure", block_delim)
63
62
  end
64
63
 
65
64
  def figure_name(elem)
@@ -22,16 +22,24 @@ module IsoDoc
22
22
  def index1(docxml, indexsect, index)
23
23
  c = indexsect.add_child("<ul></ul>").first
24
24
  index.keys.sort.each do |k|
25
- words = index[k].keys.each_with_object({}) do |w, v|
26
- v[sortable(w).downcase] = w
27
- end
25
+ words = index_sort_buckets(index[k].keys)
28
26
  words.keys.localize(@lang.to_sym).sort.to_a.each do |w|
29
- c.add_child index_entries(words, index[k], w)
27
+ words[w].each { |w1| c.add_child index_entries(w1, index[k]) }
30
28
  end
31
29
  end
32
30
  index1_cleanup(docxml)
33
31
  end
34
32
 
33
+ # Sorting is case-insensitive, so entries differing only in case (or
34
+ # only in markup, which the sort key strips) share a sort key; they
35
+ # are kept as distinct entries in a bucket per key, sorted adjacently
36
+ # (metanorma/metanorma#298: overwriting them lost all but one entry)
37
+ def index_sort_buckets(keys)
38
+ keys.compact.each_with_object({}) do |w, v|
39
+ (v[sortable(w).downcase] ||= []) << w
40
+ end.transform_values(&:sort)
41
+ end
42
+
35
43
  def index1_cleanup(docxml)
36
44
  docxml.xpath(ns("//indexsect//xref")).each do |x|
37
45
  x.children.remove
@@ -48,34 +56,31 @@ module IsoDoc
48
56
  { xref_lbl: ", ", see_lbl: ", #{see_lbl}", also_lbl: ", #{also_lbl}" }
49
57
  end
50
58
 
51
- def index_entries(words, index, primary)
52
- ret = index_entries_head(words[primary],
53
- index.dig(words[primary], nil, nil),
59
+ def index_entries(primary, index)
60
+ ret = index_entries_head(primary, index.dig(primary, nil, nil),
54
61
  index_entries_opt)
55
- words2 = index[words[primary]]&.keys&.compact
56
- &.each_with_object({}) { |w, v| v[w.downcase] = w }
62
+ words2 = index_sort_buckets(index[primary].keys)
57
63
  unless words2.empty?
58
64
  ret += "<ul>"
59
65
  words2.keys.localize(@lang.to_sym).sort.to_a.each do |w|
60
- ret += index_entries2(words2, index[words[primary]], w)
66
+ words2[w].each { |w1| ret += index_entries2(w1, index[primary]) }
61
67
  end
62
68
  ret += "</ul>"
63
69
  end
64
70
  "#{ret}</li>"
65
71
  end
66
72
 
67
- def index_entries2(words, index, secondary)
68
- ret = index_entries_head(words[secondary],
69
- index.dig(words[secondary], nil),
73
+ def index_entries2(secondary, index)
74
+ ret = index_entries_head(secondary, index.dig(secondary, nil),
70
75
  index_entries_opt)
71
- words3 = index[words[secondary]]&.keys&.compact
72
- &.each_with_object({}) { |w, v| v[w.downcase] = w }
76
+ words3 = index_sort_buckets(index[secondary].keys)
73
77
  unless words3.empty?
74
78
  ret += "<ul>"
75
79
  words3.keys.localize(@lang.to_sym).sort.to_a.each do |w|
76
- ret += (index_entries_head(words3[w],
77
- index[words[secondary]][words3[w]],
78
- index_entries_opt) + "</li>")
80
+ words3[w].each do |w1|
81
+ ret += "#{index_entries_head(w1, index[secondary][w1],
82
+ index_entries_opt)}</li>"
83
+ end
79
84
  end
80
85
  ret += "</ul>"
81
86
  end
@@ -95,12 +100,9 @@ module IsoDoc
95
100
 
96
101
  def index_entries_see(entries, label)
97
102
  see_sort = entries&.dig(label) or return nil
98
- x = see_sort.each_with_object({}) do |w, v|
99
- v[sortable(w).downcase] = w
100
- end
101
- x.keys.localize(@lang.to_sym).sort.to_a.map do |k|
102
- # see_sort[k]
103
- x[k]
103
+ x = index_sort_buckets(see_sort)
104
+ x.keys.localize(@lang.to_sym).sort.to_a.flat_map do |k|
105
+ @index_fold_case ? [index_fold_head(x[k])] : x[k].uniq
104
106
  end.join(", ")
105
107
  end
106
108
 
@@ -116,12 +118,54 @@ module IsoDoc
116
118
  index = extract_indexterms(terms)
117
119
  index = extract_indexsee(index, see, :see)
118
120
  index = extract_indexsee(index, also, :also)
121
+ @index_fold_case and index_fold_case(index)
119
122
  index.keys.sort.each_with_object({}) do |k, v|
120
123
  v[sortable(k)[0].upcase.transliterate] ||= {}
121
124
  v[sortable(k)[0].upcase.transliterate][k] = index[k]
122
125
  end
123
126
  end
124
127
 
128
+ # Entries differing only in case merge into a single headword, the
129
+ # lowercase spelling, with the union of their subentries and locators:
130
+ # ((activity)) mid-sentence and ((Activity)) sentence-initially are
131
+ # the same entry. Suppressed by the :index-case-sensitive: document
132
+ # attribute for identifier-heavy documents, where Activity and
133
+ # activity are distinct case-bearing names (metanorma/metanorma#298).
134
+ # Applied per level: primaries, then secondaries, then tertiaries.
135
+ def index_fold_case(index)
136
+ index_fold_level(index)
137
+ index.each_value do |sec|
138
+ index_fold_level(sec)
139
+ sec.each_value { |ter| index_fold_level(ter) }
140
+ end
141
+ end
142
+
143
+ def index_fold_level(hash)
144
+ hash.keys.grep(String).group_by { |w| sortable(w).downcase }
145
+ .each_value { |ws| ws.size > 1 and index_fold_bucket(hash, ws) }
146
+ end
147
+
148
+ def index_fold_bucket(hash, words)
149
+ head = index_fold_head(words)
150
+ (words - [head]).each do |w|
151
+ index_merge(hash[head], hash[w])
152
+ hash.delete(w)
153
+ end
154
+ end
155
+
156
+ def index_fold_head(words)
157
+ words.find { |w| sortable(w) == sortable(w).downcase } || words.min
158
+ end
159
+
160
+ def index_merge(target, other)
161
+ other.each do |k, v|
162
+ if !target.key?(k) then target[k] = v
163
+ elsif v.is_a?(Array) then target[k].concat(v)
164
+ else index_merge(target[k], v)
165
+ end
166
+ end
167
+ end
168
+
125
169
  def extract_indexsee(val, terms, label)
126
170
  terms.each_with_object(val) do |t, v|
127
171
  term, term2, term3 = extract_indexterms_init(t)
@@ -68,6 +68,9 @@ module IsoDoc
68
68
 
69
69
  def ol_label_format(semx, elem)
70
70
  template = ol_label_template(elem)[elem.parent["type"].to_sym]
71
+ # types outside the template set are not decorated, e.g. xml2rfc v2
72
+ # format strings passed through as ol/@type by metanorma-ietf
73
+ template or return semx
71
74
  template.sub("%", semx)
72
75
  end
73
76
 
@@ -34,7 +34,7 @@ module IsoDoc
34
34
  def clausedelim
35
35
  ret = super
36
36
  (ret && !ret.empty?) or return ret
37
- "<span class='fmt-autonum-delim'>#{ret}</span>"
37
+ "<span class='fmt-clause-delim'>#{ret}</span>"
38
38
  end
39
39
 
40
40
  def clause1(elem)
@@ -175,17 +175,35 @@ module IsoDoc
175
175
  lexer = Rouge::Lexer.find(lang || "plaintext") ||
176
176
  Rouge::Lexer.find("plaintext")
177
177
  l = Rouge::Lexers::Escape.new(start: "{^^{", end: "}^^}", lang: lexer)
178
+ source_collapse_markup_newlines(elem)
178
179
  source = to_xml(elem.children).gsub("<", "{^^{<").gsub(">", ">}^^}")
179
180
  l.lang.reset!
180
181
  l.lex(@c.decode(source))
181
182
  end
182
183
 
184
+ # Embedded markup serialised across multiple lines (e.g. pretty-printed
185
+ # MathML from stem) would be split across line cells by the line-table
186
+ # formatter, leaving each cell with unbalanced tags: the string reparse
187
+ # of the table then cascades mis-nested rows, and rendering crashes on
188
+ # the orphaned rows (metanorma-pdfa#84). Collapse whitespace inside
189
+ # embedded elements so each occupies a single code line; code text
190
+ # outside the elements is untouched.
191
+ def source_collapse_markup_newlines(elem)
192
+ elem.elements.each do |e|
193
+ e.xpath(".//text()").each do |t|
194
+ /[\n\r]/.match?(t.content) and
195
+ t.content = t.content.gsub(/\s*[\n\r]+\s*/, " ")
196
+ end
197
+ end
198
+ end
199
+
183
200
  def source_label(elem)
184
201
  if !labelled_ancestor(elem) && # do not number if labelled_ancestor
185
202
  lbl = @xrefs.anchor(elem["id"], :label, false)
186
203
  s = labelled_autonum(lower2cap(@i18n.figure), elem["id"], lbl)&.strip
187
204
  end
188
- prefix_name(elem, { caption: block_delim }, s, "name")
205
+ prefix_name(elem, { caption: i18n_delim("sourcecode", block_delim) }, s,
206
+ "name")
189
207
  end
190
208
  end
191
209
  end
@@ -38,7 +38,7 @@ module IsoDoc
38
38
  end
39
39
 
40
40
  def termnote_delim(_elem, lbl)
41
- l10n(": ", { prev: lbl })
41
+ l10n(i18n_delim("termnote", ": "), { prev: lbl })
42
42
  end
43
43
 
44
44
  def termnote1(elem)
@@ -1,3 +1,3 @@
1
1
  module IsoDoc
2
- VERSION = "3.7.1".freeze
2
+ VERSION = "3.7.2".freeze
3
3
  end
@@ -135,6 +135,7 @@ title_prefixes:
135
135
  locality: {
136
136
  section: Section,
137
137
  clause: Clause,
138
+ definition: Definition,
138
139
  part: Part,
139
140
  paragraph: Paragraph,
140
141
  chapter: Chapter,
@@ -174,6 +175,9 @@ inflection:
174
175
  Clause:
175
176
  sg: Clause
176
177
  pl: Clauses
178
+ Definition:
179
+ sg: Definition
180
+ pl: Definitions
177
181
  Annex:
178
182
  sg: Annex
179
183
  pl: Annexes
@@ -131,6 +131,7 @@ title_prefixes:
131
131
  locality: {
132
132
  section: Section,
133
133
  clause: Article,
134
+ definition: Définition,
134
135
  part: Partie,
135
136
  paragraph: Alinéa,
136
137
  chapter: Chapitre,
@@ -170,6 +171,9 @@ inflection:
170
171
  Clause:
171
172
  sg: Article
172
173
  pl: Articles
174
+ Definition:
175
+ sg: Définition
176
+ pl: Définitions
173
177
  Annex:
174
178
  sg: Annexe
175
179
  pl: Annexes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isodoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.1
4
+ version: 3.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -351,7 +351,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
351
351
  - !ruby/object:Gem::Version
352
352
  version: '0'
353
353
  requirements: []
354
- rubygems_version: 4.0.10
354
+ rubygems_version: 4.0.16
355
355
  specification_version: 4
356
356
  summary: Convert documents in IsoDoc into Word and HTML in AsciiDoc.
357
357
  test_files: []