metanorma-standoc 3.4.7 → 3.4.9

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: b1dfc55e13dc35cba30f131721a669b4d8f4373d9afea12d2a1fd19c178ab9b1
4
- data.tar.gz: b1c07c99802d3018f9205e3b68574d2e6557380d81b51f6d33fec5026da355d4
3
+ metadata.gz: a76abb834a587adae31dae92f93936ca82968bc7b77a9d907633b51a0de29319
4
+ data.tar.gz: a03fe3c22052427818cc8161c376786c27b3ee570dec4f4fa243713b54ef30a6
5
5
  SHA512:
6
- metadata.gz: 501724517b1124aab3b9be9dc9eece4c4da9671e8ca13b6c4a86f52e45487772ed50b51418cc334280f183241c0d244acc4b7f63040a6b8ecf0ac5e48676e458
7
- data.tar.gz: 3b6a32f1e821f68243cc01f3833f6f8fa26f14ac5f6b686e10cf872c6f4cdd968324579540dea0ee481d06ecaa04695390a601888d72977ea1375dabb1b4b789
6
+ metadata.gz: 17ac7b991acf83f47f01e79c8c0eb71f1531db814c77ec60b5fa69b0828707a25702f83a92492b49a2217f23525287d22fb6e064b3a48a3b3ecbbc8646a964d4
7
+ data.tar.gz: df7537f969e7559c90569125da0c3847da7cc723c998f96e1393229607d22271191b909c36262a8ee89a0c63fea20b5a4e005fa925e430c6b2e6b08d6699c1a6
data/.rubocop.yml CHANGED
@@ -2,9 +2,30 @@
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
14
+
15
+ # Rubocop plugins enabled centrally so every metanorma-org gem picks them up
16
+ # on cimas sync — best practice belongs at the shared-template layer, not
17
+ # per-repo. Per ronaldtse feedback on metanorma/ci#332.
18
+ plugins:
19
+ - rubocop-rspec
20
+ - rubocop-performance
21
+ - rubocop-rake
5
22
 
6
23
  # local repo-specific modifications
7
24
  # ...
8
25
 
9
26
  AllCops:
10
- TargetRubyVersion: 3.4
27
+ # 3.3 matches the org-wide minimum being pushed via #274 (Raise minimum
28
+ # Ruby version to 3.3 due to EOL of 3.2 on 2026-03-31). Was 3.4 before
29
+ # this commit — 3.4 was above the org's stated minimum and would have
30
+ # applied Rubocop rules that fail-close on gems still targeting 3.3.
31
+ TargetRubyVersion: 3.3
data/Gemfile CHANGED
@@ -6,5 +6,10 @@ git_source(:github) { |repo| "https://github.com/#{repo}" }
6
6
 
7
7
  gemspec
8
8
 
9
- eval_gemfile("Gemfile.devel") rescue nil
9
+ # Stopgap: lutaml 0.11.x removed lib/lutaml/xmi.rb, which metanorma-plugin-lutaml
10
+ # 0.7.x still `require`s. Hold lutaml at 0.10.x until plugin-lutaml follows the
11
+ # file to its new home. Remove once resolved:
12
+ # https://github.com/metanorma/metanorma-plugin-lutaml/issues/292
13
+ gem "lutaml", "< 0.11"
10
14
 
15
+ eval_gemfile("Gemfile.devel") rescue nil
@@ -150,6 +150,11 @@ a.FootnoteRef, span.FootnoteRef {
150
150
  vertical-align: super;
151
151
  }
152
152
 
153
+ a.TableFootnoteRef, a.FootnoteRef, a.footnote-number,
154
+ sup a, a:has(> sup) {
155
+ vertical-align: baseline;
156
+ }
157
+
153
158
  .addition {
154
159
  color: blue;
155
160
  }
@@ -198,15 +198,23 @@ module Metanorma
198
198
  XML
199
199
  end
200
200
 
201
- def published_base?(stage, _xmldoc)
201
+ def published_base?(stage, xmldoc)
202
202
  if @stage_published
203
203
  @stage_published == "true"
204
- else published?(stage, _xmldoc)
204
+ else !draft_stage?(stage, xmldoc)
205
205
  end
206
206
  end
207
207
 
208
+ # Is the stage a draft (unpublished) stage? Overridable seam for
209
+ # flavours whose stage repertoires are not the binary
210
+ # published/unpublished; the :docstage-published: document
211
+ # attribute (@stage_published, above) trumps it either way.
212
+ def draft_stage?(stage, xmldoc)
213
+ !published?(stage, xmldoc)
214
+ end
215
+
208
216
  def published?(stage, _xmldoc)
209
- stage.casecmp("published").zero?
217
+ stage.to_s.casecmp("published").zero?
210
218
  end
211
219
 
212
220
  # allows us to deal with doc relation localities,
@@ -61,7 +61,7 @@ module Metanorma
61
61
  boilerplateauthority embed_hdr embed_id erefstyle originstyle
62
62
  xrefstyle blockunnumbered keepasciimath numberfmt_formula
63
63
  numberfmt_prof sort_biblio reqt_models default_requirement_model
64
- document_scheme
64
+ document_scheme publisher_sort_config
65
65
  ]
66
66
  end
67
67
 
@@ -34,6 +34,77 @@ module Metanorma
34
34
  bib
35
35
  end
36
36
 
37
+ BIBLIO_PUBLISHER_ORG =
38
+ "./contributor[role/@type = 'publisher']/organization".freeze
39
+
40
+ # Configurable publisher sort ranking, shared by every flavour's
41
+ # bibliography sort. `table` is the flavour's default ordering, an array
42
+ # of { abbrev:, name:, rank: }. A per-document / metanorma-taste override
43
+ # (:sort-biblio-<abbrev>: <rank>: <name> attributes, captured into
44
+ # @publisher_sort_config) REPLACES the flavour default when present.
45
+ #
46
+ # A matched publisher gets its configured rank; an unmatched standards
47
+ # reference sorts immediately after the ranked publishers, and a
48
+ # non-standard reference last. With no override and the flavour's own
49
+ # default table, this reproduces the flavour's historical hardcoded ranks.
50
+ def publisher_sort_rank(bib, table)
51
+ table = publisher_sort_table(table)
52
+ if (entry = publisher_sort_match(bib, table))
53
+ return entry[:rank]
54
+ end
55
+
56
+ maxrank = table.map { |e| e[:rank] }.max || 0
57
+ biblio_standards_ref?(bib) ? maxrank + 1 : maxrank + 2
58
+ end
59
+
60
+ # Secondary sort token: the co-publisher(s) other than the matched
61
+ # primary. Deterministic when several co-publishers exist (returns the
62
+ # smallest token), which reproduces e.g. JIS's "JIS+IEC before
63
+ # JIS+ISO, JIS+IEC+ISO alongside JIS+IEC" ordering.
64
+ def publisher_sort_second(bib, table)
65
+ entry = publisher_sort_match(bib, publisher_sort_table(table))
66
+ entry or return ""
67
+ bib.xpath(BIBLIO_PUBLISHER_ORG)
68
+ .reject { |o| publisher_org_match?(o, entry) }
69
+ .map { |o| (o.at("./abbreviation") || o.at("./name"))&.text&.strip }
70
+ .compact.reject(&:empty?).min || ""
71
+ end
72
+
73
+ def publisher_sort_table(default_table)
74
+ cfg = @publisher_sort_config
75
+ cfg && !cfg.empty? ? cfg : default_table
76
+ end
77
+
78
+ # lowest-ranked table entry whose publisher appears on the bibitem
79
+ def publisher_sort_match(bib, table)
80
+ orgs = bib.xpath(BIBLIO_PUBLISHER_ORG)
81
+ table.sort_by { |e| e[:rank] }.find do |e|
82
+ orgs.any? { |o| publisher_org_match?(o, e) }
83
+ end
84
+ end
85
+
86
+ # A table entry matches a publisher organization on its abbreviation
87
+ # (case-insensitive, since AsciiDoc lowercases attribute-derived abbrevs)
88
+ # or on any of its names (`name` may be a single string or an array; the
89
+ # latter lets a flavour list, e.g., a Japanese and an English publisher
90
+ # name for the same rank).
91
+ def publisher_org_match?(org, entry)
92
+ abbr = org.at("./abbreviation")&.text&.strip
93
+ name = org.at("./name")&.text&.strip
94
+ names = Array(entry[:name]).map { |n| n.to_s.strip }.reject(&:empty?)
95
+ (!entry[:abbrev].to_s.empty? && abbr&.casecmp?(entry[:abbrev])) ||
96
+ (!name.to_s.empty? && names.include?(name))
97
+ end
98
+
99
+ # Whether a bibitem is a standards reference (sorts after the ranked
100
+ # publishers, before non-standard references). Base definition: it has a
101
+ # typed, non-DOI/ISSN/ISBN docidentifier. Flavours may widen this (ISO
102
+ # also treats an untyped docidentifier as a standards reference).
103
+ def biblio_standards_ref?(bib)
104
+ !!bib.at("./docidentifier[@type][not(#{@conv.skip_docid} or " \
105
+ "@type = 'metanorma')]")
106
+ end
107
+
37
108
  # default presuppose that all citations in biblio numbered
38
109
  # consecutively, but that standards codes are preserved as is:
39
110
  # only numeric references are renumbered
@@ -48,6 +48,7 @@ module Metanorma
48
48
  def concept_cleanup
49
49
  xmldoc.xpath("//concept").each do |n|
50
50
  refterm = n.at("./refterm") or next
51
+ refterm.xpath(".//index").each(&:remove)
51
52
  lookup = norm_ref_id_text(refterm.text.strip)
52
53
  p = @lookup[:sec2prim][lookup] and refterm.children = @c.encode(p)
53
54
  end
@@ -116,6 +116,7 @@ module Metanorma
116
116
  .merge(align: node.attr("align"),
117
117
  variant_title: node.role == "variant-title" ? true : nil,
118
118
  key: node.option?("key") ? "true" : nil,
119
+ class: node.attr("class"),
119
120
  type: node.attr("type"))))
120
121
  end
121
122
 
@@ -134,7 +135,7 @@ module Metanorma
134
135
 
135
136
  def quote_attrs(node)
136
137
  attr_code(id_attr(node).merge(keep_attrs(node))
137
- .merge(align: node.attr("align")))
138
+ .merge(align: node.attr("align"), class: node.attr("class")))
138
139
  end
139
140
 
140
141
  def quote_attribution(node, out)
@@ -164,6 +165,7 @@ module Metanorma
164
165
  linenums: linenums ? "true" : nil,
165
166
  unnumbered: node.option?("unnumbered") ? "true" : nil,
166
167
  number: node.attr("number"),
168
+ class: node.attr("class"),
167
169
  filename: node.attr("filename"))))
168
170
  end
169
171
 
@@ -41,7 +41,8 @@ module Metanorma
41
41
  def pseudocode_example(node)
42
42
  node.blocks.each { |b| b.remove_sub(:replacements) }
43
43
  noko do |xml|
44
- xml.figure **example_attrs(node).merge(class: "pseudocode") do |ex|
44
+ klass = (["pseudocode"] + (node.attr("class")&.split || [])).uniq
45
+ xml.figure **example_attrs(node).merge(class: klass.join(" ")) do |ex|
45
46
  block_title(node, ex)
46
47
  wrap_in_para(node, ex)
47
48
  end
@@ -50,7 +51,8 @@ module Metanorma
50
51
 
51
52
  def example_attrs(node)
52
53
  attr_code(id_unnum_attrs(node).merge(keep_attrs(node))
53
- .merge(collapsible: node.option?("collapsible") ? "true" : nil))
54
+ .merge(collapsible: node.option?("collapsible") ? "true" : nil,
55
+ class: node.attr("class")))
54
56
  end
55
57
 
56
58
  def example_proper(node)
@@ -12,7 +12,7 @@ module Metanorma
12
12
 
13
13
  def note_attrs(node)
14
14
  attr_code(termnote_attrs(node).merge(admonition_core_attrs(node)
15
- .merge(type: node.attr("type"))))
15
+ .merge(type: node.attr("type"), class: node.attr("class"))))
16
16
  end
17
17
 
18
18
  def sidebar(node)
@@ -86,7 +86,7 @@ module Metanorma
86
86
  def admonition_attrs(node)
87
87
  attr_code(keep_attrs(node).merge(id_attr(node)
88
88
  .merge(admonition_core_attrs(node)
89
- .merge(type: admonition_name(node)))))
89
+ .merge(type: admonition_name(node), class: node.attr("class")))))
90
90
  end
91
91
 
92
92
  def admonition_core_attrs(node)
@@ -40,11 +40,16 @@ module Metanorma
40
40
  preprocessor Metanorma::Standoc::EmbedIncludeProcessor
41
41
  preprocessor Metanorma::Standoc::LinkProtectPreprocessor
42
42
  preprocessor Metanorma::Standoc::PassProtectPreprocessor
43
- preprocessor Metanorma::Standoc::MonospaceProtectPreprocessor
44
43
  preprocessor Metanorma::Plugin::Lutaml::Json2TextPreprocessor
45
44
  preprocessor Metanorma::Plugin::Lutaml::Yaml2TextPreprocessor
46
45
  preprocessor Metanorma::Plugin::Lutaml::Data2TextPreprocessor
47
46
  preprocessor Metanorma::Plugin::Lutaml::LutamlXmiUmlPreprocessor
47
+ # MonospaceProtect must run AFTER the content-injecting preprocessors
48
+ # (json2text/yaml2text/data2text, XMI) so that monospace in their
49
+ # injected content is protected from Asciidoctor character
50
+ # substitution too, consistently with hand-authored adoc.
51
+ # metanorma/iso-10303#208
52
+ preprocessor Metanorma::Standoc::MonospaceProtectPreprocessor
48
53
  preprocessor Metanorma::Plugin::Glossarist::DatasetPreprocessor
49
54
  preprocessor Metanorma::Standoc::NamedEscapePreprocessor
50
55
  inline_macro Metanorma::Standoc::PreferredTermInlineMacro
@@ -106,7 +111,8 @@ module Metanorma
106
111
  $xreftext = {}
107
112
 
108
113
  attr_reader :bibdb, :lang, :script, :locale, :relaton_log, :libdir, :backend,
109
- :novalid, :local_log, :output_dir, :filename, :files_to_delete
114
+ :novalid, :local_log, :output_dir, :filename, :files_to_delete,
115
+ :docstage_valid
110
116
 
111
117
  def initialize(backend, opts)
112
118
  doc = opts&.dig(:document)
@@ -27,6 +27,7 @@ module Metanorma
27
27
  @document_scheme = document_scheme(node)
28
28
  @default_doctype = "standard"
29
29
  @stage_published = node.attr("docstage-published")
30
+ @docstage_valid = node.attr("docstage-valid")&.split(/,\s*/)
30
31
  @metadata_attrs = metadata_attrs(node)
31
32
  end
32
33
 
@@ -116,6 +117,11 @@ module Metanorma
116
117
  end
117
118
 
118
119
  def init_toc(node)
120
+ init_toc_levels(node)
121
+ init_toc_types(node)
122
+ end
123
+
124
+ def init_toc_levels(node)
119
125
  @htmltoclevels = node.attr("toclevels-html") ||
120
126
  node.attr("htmltoclevels") ||
121
127
  node.attr("toclevels") || toc_default[:html_levels]
@@ -125,10 +131,13 @@ module Metanorma
125
131
  @pdftoclevels = node.attr("toclevels-pdf") ||
126
132
  node.attr("toclevels") || toc_default[:pdf_levels]
127
133
  @toclevels = node.attr("toclevels") || toc_default[:html_levels]
128
- @tocfigures = node.attr("toc-figures")
129
- @toctables = node.attr("toc-tables")
130
- @tocrecommendations = node.attr("toc-recommendations")
131
- @tocexamples = node.attr("toc-examples")
134
+ end
135
+
136
+ def init_toc_types(node)
137
+ @tocfigures = node.attr("toc-figures") != "false"
138
+ @toctables = node.attr("toc-tables") != "false"
139
+ @tocrecommendations = node.attr("toc-recommendations") != "false"
140
+ @tocexamples = node.attr("toc-examples") != "false"
132
141
  end
133
142
 
134
143
  def toc_default
@@ -175,6 +184,7 @@ module Metanorma
175
184
  @no_isobib = node.attr("no-isobib")
176
185
  @flush_caches = node.attr("flush-caches")
177
186
  @sort_biblio = node.attr("sort-biblio") != "false"
187
+ @publisher_sort_config = init_publisher_sort_config(node)
178
188
  init_bib_db(node)
179
189
  init_bib_caches(node)
180
190
  init_iev_caches(node)
@@ -182,6 +192,21 @@ module Metanorma
182
192
  @biblio_cutoff = biblio_cutoff(node)
183
193
  end
184
194
 
195
+ # Per-publisher bibliography sort ranks, from
196
+ # :sort-biblio-<abbrev>: <rank>: <name> document attributes (set directly
197
+ # in a document or injected by a metanorma-taste config). AsciiDoc
198
+ # lowercases attribute names, so <abbrev> is matched case-insensitively
199
+ # downstream. Returns [] when none are set (flavours keep their default
200
+ # publisher ordering).
201
+ def init_publisher_sort_config(node)
202
+ node.attributes.each_with_object([]) do |(k, v), acc|
203
+ m = /\Asort-biblio-(?<abbrev>.+)\z/.match(k.to_s) or next
204
+ rank_s, name = v.to_s.split(":", 2)
205
+ rank = Integer(rank_s.to_s.strip, exception: false) or next
206
+ acc << { abbrev: m[:abbrev].strip, name: name&.strip, rank: rank }
207
+ end
208
+ end
209
+
185
210
  def init_bib_db(node)
186
211
  @bibdb = nil
187
212
  @local_bibdb =
@@ -197,6 +222,15 @@ module Metanorma
197
222
  end
198
223
 
199
224
  def biblio_cutoff(node)
225
+ # An explicit :bibliography-cutoff-date: overrides the date derived from
226
+ # the document's own dates, and a sentinel (none/false/no/off) disables
227
+ # the cutoff entirely. metanorma/metanorma-standoc#941.
228
+ if (override = node.attr("bibliography-cutoff-date"))
229
+ override = override.strip
230
+ return nil if %w(none false no off).include?(override.downcase)
231
+
232
+ return complete_and_compare_dates([override])
233
+ end
200
234
  dates = %w(revdate published-date accessed-date created-date
201
235
  implemented-date confirmed-date updated-date issued-date
202
236
  circulated-date unchanged-date copyright-year)
@@ -24,6 +24,7 @@ module Metanorma
24
24
  def ul_attrs(node)
25
25
  attr_code(id_attr(node).merge(keep_attrs(node)
26
26
  .merge(display: node.attr("display"),
27
+ class: node.attr("class"),
27
28
  "display-directives": node.attr("display-directives"))))
28
29
  end
29
30
 
@@ -63,6 +64,7 @@ module Metanorma
63
64
  .merge(type: olist_style(node.style),
64
65
  start: node.attr("start"),
65
66
  display: node.attr("display"),
67
+ class: node.attr("class"),
66
68
  "display-directives": node.attr("display-directives"),
67
69
  "explicit-type": olist_style(node.attributes[1]))))
68
70
  end
@@ -101,6 +103,7 @@ module Metanorma
101
103
  .merge(
102
104
  metadata: node.option?("metadata") ? "true" : nil,
103
105
  key: node.option?("key") ? "true" : nil,
106
+ class: node.attr("class"),
104
107
  )))
105
108
  end
106
109
 
@@ -67,16 +67,23 @@ module Metanorma
67
67
  end.join
68
68
  end
69
69
 
70
- # debugging output of results of all preprocessing,
71
- # including include files concatenation and Lutaml/Liquid processing
70
+ # debugging output of results of all preprocessing, including include
71
+ # files concatenation and Lutaml/Liquid processing.
72
+ #
73
+ # Skipped for docfile-less (string/stdin) input: the target would
74
+ # degenerate to a single CWD-relative "./metanorma.asciidoc.log.txt" that
75
+ # the thousands of collection sub-compiles all truncate-rewrite (so it
76
+ # carries no useful diagnostics anyway). And a diagnostic write must never
77
+ # abort the build, so a write failure degrades to a warning rather than
78
+ # propagating out of the preprocessor. metanorma/metanorma-standoc#1209
72
79
  def log(doc, text)
73
- source = doc.attr("docfile") || "metanorma"
80
+ source = doc.attr("docfile") or return
74
81
  dirname = File.dirname(source)
75
82
  basename = File.basename(source, ".*")
76
83
  fname = File.join(dirname, "#{basename}.asciidoc.log.txt")
77
- File.open(fname, "w:UTF-8") do |f|
78
- f.write(text.join("\n"))
79
- end
84
+ File.open(fname, "w:UTF-8") { |f| f.write(text.join("\n")) }
85
+ rescue SystemCallError => e
86
+ warn "metanorma: could not write preprocessing log #{fname}: #{e.message}"
80
87
  end
81
88
  end
82
89
 
@@ -9,7 +9,11 @@ module Metanorma
9
9
  plain: node.option?("plain") ? "true" : nil,
10
10
  style: node.attr("css-style"),
11
11
  summary: node.attr("summary"),
12
- width: node.attr("width"))
12
+ width: node.attr("width"),
13
+ # Custom CSS class(es) passed through to HTML output, via the
14
+ # `[class="..."]` named attribute (NOT role, which is reserved
15
+ # for block dispatch). metanorma/metanorma-pdfa#33
16
+ class: node.attr("class"))
13
17
  end
14
18
 
15
19
  def table(node)
@@ -19,6 +19,6 @@ module Metanorma
19
19
  end
20
20
 
21
21
  module Standoc
22
- VERSION = "3.4.7".freeze
22
+ VERSION = "3.4.9".freeze
23
23
  end
24
24
  end
@@ -1902,15 +1902,40 @@ or as the string "auto"</a:documentation>
1902
1902
  <a:documentation>Mathematically formatted text</a:documentation>
1903
1903
  <element name="stem">
1904
1904
  <ref name="StemAttributes"/>
1905
- <oneOrMore>
1906
- <choice>
1905
+ <optional>
1906
+ <text>
1907
1907
  <a:documentation>The content of the mathematically formatted text</a:documentation>
1908
- <text/>
1909
- <ref name="AnyElement"/>
1910
- </choice>
1911
- </oneOrMore>
1908
+ </text>
1909
+ </optional>
1910
+ <optional>
1911
+ <ref name="mathml"/>
1912
+ </optional>
1913
+ <optional>
1914
+ <ref name="asciimath"/>
1915
+ </optional>
1916
+ <optional>
1917
+ <ref name="latexmath"/>
1918
+ </optional>
1912
1919
  </element>
1913
1920
  </define>
1921
+ <define name="latexmath">
1922
+ <a:documentation>Encoding of LatexMath</a:documentation>
1923
+ <element name="latexmath">
1924
+ <text/>
1925
+ </element>
1926
+ </define>
1927
+ <define name="asciimath">
1928
+ <a:documentation>Encoding of AsciiMath</a:documentation>
1929
+ <element name="asciimath">
1930
+ <text/>
1931
+ </element>
1932
+ </define>
1933
+ <define name="mathml">
1934
+ <a:documentation>Encoding of MathML: the official W3C MathML 4 grammar (namespace
1935
+ http://www.w3.org/1998/Math/MathML), via the metanorma wrapper in
1936
+ grammars/mathml/. See grammars/mathml/README.adoc and basicdoc-models#35.</a:documentation>
1937
+ <externalRef href="metanorma-mathml.rng"/>
1938
+ </define>
1914
1939
  <define name="StemAttributes">
1915
1940
  <attribute name="type">
1916
1941
  <a:documentation>The notation used to mathematically format the text</a:documentation>
@@ -852,7 +852,7 @@ titlecase, or lowercase</a:documentation>
852
852
  <a:documentation>Width of the figure block in rendering</a:documentation>
853
853
  </attribute>
854
854
  </optional>
855
- <ref name="BlockAttributes"/>
855
+ <ref name="BlockAttributesCore"/>
856
856
  </define>
857
857
  <define name="SourceAttributes" combine="interleave">
858
858
  <ref name="BlockAttributes"/>
@@ -1283,7 +1283,11 @@ That concept may be defined as a term within the current document, or it may be
1283
1283
  <a:documentation>Class of input form</a:documentation>
1284
1284
  </attribute>
1285
1285
  </optional>
1286
- <ref name="BlockAttributes"/>
1286
+ <ref name="BlockAttributesCore">
1287
+ <a:documentation>form declares its own class above, so it uses BlockAttributesCore to
1288
+ avoid duplicating the custom-class slot in BlockAttributes. Treated like
1289
+ figure. See metanorma/metanorma-standoc#1197</a:documentation>
1290
+ </ref>
1287
1291
  <zeroOrMore>
1288
1292
  <!-- Input form contents -->
1289
1293
  <choice>
@@ -2768,7 +2772,11 @@ links within an SVG file, so that the SVG file can hyperlink to anchors within t
2768
2772
  </oneOrMore>
2769
2773
  </element>
2770
2774
  </define>
2771
- <define name="BlockAttributes">
2775
+ <define name="BlockAttributesCore">
2776
+ <a:documentation>Block attributes excluding the custom CSS class. Blocks that already carry
2777
+ their own `class` attribute (e.g. figure, via basicdoc FigureAttributes)
2778
+ include this directly, to avoid a duplicate-attribute collision with the
2779
+ `class` added in BlockAttributes. See metanorma/metanorma-standoc#1197</a:documentation>
2772
2780
  <optional>
2773
2781
  <attribute name="keep-with-next">
2774
2782
  <a:documentation>Keep this block on the same page as the following block in paged media</a:documentation>
@@ -2800,6 +2808,17 @@ to span across both columns</a:documentation>
2800
2808
  </attribute>
2801
2809
  </optional>
2802
2810
  </define>
2811
+ <define name="BlockAttributes">
2812
+ <a:documentation>Universal block attributes, including a custom CSS class (space-separated,
2813
+ appended additively to the block's built-in HTML class, HTML output only).
2814
+ See metanorma/metanorma-standoc#1197</a:documentation>
2815
+ <ref name="BlockAttributesCore"/>
2816
+ <optional>
2817
+ <attribute name="class">
2818
+ <a:documentation>Custom CSS class(es) for the block in HTML output</a:documentation>
2819
+ </attribute>
2820
+ </optional>
2821
+ </define>
2803
2822
  <define name="DisplayDirective">
2804
2823
  <a:documentation>Directive on how to render a block in Presentation XML </a:documentation>
2805
2824
  <optional>