metanorma 2.4.3 → 2.5.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.
@@ -4,6 +4,8 @@ require "isodoc"
4
4
  require "metanorma-utils"
5
5
  require_relative "fileparse"
6
6
  require_relative "filelocation"
7
+ require_relative "../artifact_store"
8
+ require "json"
7
9
 
8
10
  module Metanorma
9
11
  class Collection
@@ -16,9 +18,61 @@ module Metanorma
16
18
  opts = compile_options_base(identifier)
17
19
  .merge(compile_options_update(identifier))
18
20
 
21
+ err_count = @compile.errors.size
19
22
  @compile.compile file, opts
20
23
  @files.set(identifier, :outputs, {})
21
24
  file_compile_formats(filename, identifier, opts)
25
+ file_compile_verify(identifier, @compile.errors[err_count..] || [])
26
+ end
27
+
28
+ # A member whose rendering raised produces no output file, but the
29
+ # collection build used to continue, exit 0, and link the absent
30
+ # document from index.html -- silent data loss (#586). Two
31
+ # complementary checks close that hole. (1) Compile pools rescued
32
+ # error messages in @compile.errors for its caller to surface; the
33
+ # standalone CLI does, but the collection pipeline never read the
34
+ # pool. The pool is shared across members and never reset, so the
35
+ # slice appended during this member's compile is this member's
36
+ # errors -- sound while members compile sequentially (the current
37
+ # design; @compile's own format-level parallelism is joined before
38
+ # compile returns). (2) Independently of whether anything was
39
+ # pooled, verify the outputs registered for the member exist on
40
+ # disk. Failures are reported here and accumulated; the collection
41
+ # renders every remaining member before Collection#render aborts,
42
+ # so one run reports all failed members.
43
+ def file_compile_verify(identifier, errors)
44
+ # Section-split parts (marked with :parentid) register format
45
+ # slots they never produce -- their :xml slot in particular is
46
+ # never written, and under filename renaming it aliases to the
47
+ # parent's name -- so verifying them is a false positive. #586
48
+ # was scoped to the non-sectionsplit path; parts are excluded
49
+ # like their parents (which return early above).
50
+ @files.get(identifier, :parentid) and return
51
+ missing = (@files.get(identifier, :outputs) || {})
52
+ .reject { |_fmt, path| File.exist?(path) }
53
+ errors.empty? && missing.empty? and return
54
+ file_compile_failure_log(identifier, errors, missing)
55
+ msg = file_compile_failure_msg(identifier, errors, missing)
56
+ ::Metanorma::Util.log("[metanorma] Error: #{msg}", :error)
57
+ fatal_errors << msg
58
+ end
59
+
60
+ def file_compile_failure_log(identifier, errors, missing)
61
+ missing.each_value do |path|
62
+ @log&.add("METANORMA_4", nil, params: [identifier, path])
63
+ end
64
+ errors.each do |e|
65
+ @log&.add("METANORMA_5", nil, params: [identifier, e])
66
+ end
67
+ end
68
+
69
+ def file_compile_failure_msg(identifier, errors, missing)
70
+ ret = []
71
+ missing.empty? or
72
+ ret << "expected output not generated: " \
73
+ "#{missing.values.join(', ')}"
74
+ ret += errors
75
+ "Failed to render collection member #{identifier}: #{ret.join('; ')}"
22
76
  end
23
77
 
24
78
  def compile_options_base(identifier)
@@ -67,8 +121,11 @@ module Metanorma
67
121
  if @files.get(ident, :attachment) then copy_file_to_dest(ident)
68
122
  else
69
123
  file, fname = @files.targetfile_id(ident, read: true)
124
+ @preserve_unresolved && member_stored?(ident, file) and next
70
125
  warn "\n\n\n\n\nProcess #{fname}: #{DateTime.now.strftime('%H:%M:%S')}"
71
126
  collection_xml = update_xrefs(file, ident, internal_refs)
127
+ @preserve_unresolved and
128
+ store_member_artefacts(ident, file, collection_xml)
72
129
  # Strip .xml or .html extension, but NOT section numbers like .0
73
130
  fname_base = File.basename(fname)
74
131
  collection_filename = fname_base
@@ -83,6 +140,31 @@ module Metanorma
83
140
  end
84
141
  end
85
142
 
143
+ # Persist an isolated member's stub-bearing Semantic XML and its anchor
144
+ # contribution to the content-addressed store, so a later reinflation pass
145
+ # can relink the stubs and a re-run can resume without recompiling.
146
+ # Within-campaign content key for a member. (Cross-build soundness needs
147
+ # the full input closure -- source + includes + templates + schemas +
148
+ # versions; see the plan gotcha. Here the source bytes + tool version
149
+ # suffice because inputs are frozen across a single build campaign.)
150
+ def member_content_hash(source)
151
+ ArtifactStore.content_hash(source, ::Metanorma::VERSION)
152
+ end
153
+
154
+ # Resume predicate: is this member already compiled and stored for its
155
+ # current input content? If so the isolated compile can skip it.
156
+ def member_stored?(ident, source)
157
+ @artifact_store.key?(ident, member_content_hash(source), :semantic)
158
+ end
159
+
160
+ def store_member_artefacts(ident, source, semantic_xml)
161
+ hash = member_content_hash(source)
162
+ @artifact_store.write(ident, hash, :semantic, semantic_xml)
163
+ @artifact_store.write(ident, hash, :anchors,
164
+ JSON.generate(@files.get(ident, :anchors) || {}))
165
+ @keep_cache or @artifact_store.prune_superseded(ident, hash)
166
+ end
167
+
86
168
  # gather internal bibitem references
87
169
  def gather_internal_refs
88
170
  @files.keys.each_with_object({}) do |i, refs|
@@ -168,7 +250,7 @@ module Metanorma
168
250
  end
169
251
 
170
252
  def update_bibitem(bib, identifier)
171
- newbib, url = update_bibitem_prep(bib, identifier)
253
+ newbib, url, attachment = update_bibitem_prep(bib, identifier)
172
254
  newbib or return
173
255
  dest = begin
174
256
  newbib.at("./docidentifier") || newbib.at(ns("./docidentifier"))
@@ -176,28 +258,63 @@ module Metanorma
176
258
  nil
177
259
  end
178
260
  dest or dest = newbib.elements[-1]
261
+ # An attachment carries both a <uri type="attachment"> (which drives
262
+ # isodoc's fmt-link attachment="true") and the citation uri; the
263
+ # attachment uri goes first. metanorma/iso-10303#208.
264
+ attachment and dest.previous = "<uri type='attachment'>#{url}</uri>"
179
265
  dest.previous = "<uri type='citation'>#{url}</uri>"
180
266
  bib.replace(newbib)
181
267
  end
182
268
 
183
269
  def update_bibitem_prep(bib, identifier)
184
- docid = get_bibitem_docid(bib, identifier) or return [nil, nil]
270
+ docid = get_bibitem_docid(bib, identifier) or return [nil, nil, nil]
185
271
  newbib = dup_bibitem(docid, bib)
186
- url = @files.url(docid, relative: true,
187
- doc: !@files.get(docid, :attachment))
188
- # Use :outputs[:html] if available (after compilation),
189
- # otherwise convert :out_path to HTML (before compilation)
190
- current_html = @files.get(identifier, :outputs)&.dig(:html)
191
- if !current_html && (out_path = @files.get(identifier, :out_path))
192
- # Convert .xml to .html, following same logic as ref_file_xml2html
193
- current_html = if out_path.end_with?(".xml")
194
- out_path.sub(/\.xml$/, ".html")
195
- else
196
- "#{out_path}.html"
197
- end
198
- end
272
+ attachment = @files.get(docid, :attachment)
273
+ url = @files.url(docid, relative: true, doc: !attachment)
274
+ current_html = referencing_html_location(identifier, attachment)
199
275
  url = make_relative_path(current_html, url) if current_html
200
- [newbib, url]
276
+ [newbib, url, attachment]
277
+ end
278
+
279
+ # Location of the HTML that will carry the cross-reference to the target,
280
+ # used as the base for relativising the attachment/citation URL.
281
+ #
282
+ # Normally this is the referencing document's own output (:outputs[:html]
283
+ # after compilation, else :out_path converted to .html before it).
284
+ #
285
+ # But when the referencing document is sectionsplit, its content is not
286
+ # emitted at its own out_path: it is split into section files written at
287
+ # the sectionsplit output location (the collection root, unless a
288
+ # sectionsplit_filename directory is configured). Relativising an
289
+ # attachment URL against the unsplit out_path then overshoots -- the
290
+ # ../../ climbs above the split file and the attachment link breaks
291
+ # (https://github.com/metanorma/iso-10303/issues/208). So for attachment
292
+ # targets of a sectionsplit document, base the relative path on the
293
+ # sectionsplit output directory instead.
294
+ def referencing_html_location(identifier, attachment)
295
+ attachment && @files.get(identifier, :sectionsplit) and
296
+ return sectionsplit_ref_html(identifier)
297
+ document_ref_html(identifier)
298
+ end
299
+
300
+ # The split section files sit at the sectionsplit output directory: the
301
+ # collection root, or the directory of a configured sectionsplit_filename.
302
+ def sectionsplit_ref_html(identifier)
303
+ d = @files.preserve_directory_structure?(identifier)
304
+ d ? File.join(File.dirname(d), "x.html") : "x.html"
305
+ end
306
+
307
+ # Use :outputs[:html] if available (after compilation), otherwise convert
308
+ # :out_path to HTML (before compilation), following ref_file_xml2html.
309
+ def document_ref_html(identifier)
310
+ @files.get(identifier, :outputs)&.dig(:html) ||
311
+ out_path_to_html(@files.get(identifier, :out_path))
312
+ end
313
+
314
+ def out_path_to_html(out_path)
315
+ out_path or return nil
316
+ out_path.end_with?(".xml") and return out_path.sub(/\.xml$/, ".html")
317
+ "#{out_path}.html"
201
318
  end
202
319
  end
203
320
  end
@@ -17,9 +17,35 @@ module Metanorma
17
17
  class Renderer
18
18
  FORMATS = %i[html xml doc pdf pdf-portfolio].freeze
19
19
 
20
- attr_accessor :isodoc, :isodoc_presxml, :nested
20
+ attr_accessor :isodoc, :isodoc_presxml
21
21
  attr_reader :xml, :compile, :compile_options, :documents, :outdir,
22
- :manifest
22
+ :manifest, :fatal_errors
23
+
24
+ # Run the block with the renderer in nested mode, restoring the previous
25
+ # mode afterwards. Nested mode (used by sectionsplit, which re-enters this
26
+ # renderer's update_xrefs on the pre-split document) preserves unresolved
27
+ # erefs and skips the finalising reference passes.
28
+ def with_nested
29
+ saved = @nested
30
+ @nested = true
31
+ yield
32
+ ensure
33
+ @nested = saved
34
+ end
35
+
36
+ # Run the block with the renderer preserving unresolved cross-document
37
+ # references as stubs instead of stripping them -- for an isolated build of
38
+ # a collection member (compiled without the rest of its collection
39
+ # present). Unlike +with_nested+, the ordinary intra-document passes still
40
+ # run (xref_process, svgmap); only the cross-document resolve/strip is
41
+ # turned into preserve, so a later reinflation pass can relink the stubs.
42
+ def with_preserve_unresolved
43
+ saved = @preserve_unresolved
44
+ @preserve_unresolved = true
45
+ yield
46
+ ensure
47
+ @preserve_unresolved = saved
48
+ end
23
49
 
24
50
  # This is only going to render the HTML collection
25
51
  # @param xml [Metanorma::Collection] input XML collection
@@ -67,10 +93,39 @@ module Metanorma
67
93
  @final = collection.final
68
94
  @c = HTMLEntities.new
69
95
  @files_to_delete = []
96
+ # Per-member rendering failures (missing outputs, pooled compile
97
+ # errors), accumulated by file_compile_verify; Collection#render
98
+ # aborts on them once every member has rendered (#586).
99
+ @fatal_errors = []
70
100
  @nested = options[:nested]
71
101
  # if false, this is the root instance of Renderer
72
102
  # if true, then this is not the last time Renderer will be run
73
103
  # (e.g. this is sectionsplit)
104
+ # Isolated/incremental build: preserve unresolved cross-document
105
+ # references as stubs instead of resolving or stripping them, so each
106
+ # member compiles independently and a later reinflation pass relinks.
107
+ @preserve_unresolved = options[:preserve_unresolved]
108
+ # Durable content-addressed store for staged artefacts. Opt-in: a plain
109
+ # build sets neither :preserve_unresolved nor :artifact_store_dir, so it
110
+ # gets the no-op Null store and is unaffected. Staging (:preserve_unresolved)
111
+ # defaults the store directory to ArtifactStore::DEFAULT_DIRNAME when no
112
+ # :artifact_store_dir is given, so callers need not name it. (Reinflation
113
+ # reads stored stubs via the manifest, not this object, so it does not
114
+ # trigger the default -- that would only create an empty directory.)
115
+ @artifact_store =
116
+ if (dir = options[:artifact_store_dir]) || options[:preserve_unresolved]
117
+ ArtifactStore.new(dir || ArtifactStore::DEFAULT_DIRNAME)
118
+ else
119
+ NullArtifactStore.new
120
+ end
121
+ # Reinflation pass: the input is a stored stub-bearing Semantic XML that
122
+ # already has intra-doc xrefs and the document suffix applied, so run
123
+ # only the cross-document resolution, not the passes already done.
124
+ @reinflate = options[:reinflate]
125
+ # Cache retention: by default keep only each document's latest artefacts,
126
+ # pruning superseded content-hash versions on write; keep_cache retains
127
+ # every version (history / multiple states side by side).
128
+ @keep_cache = options[:keep_cache]
74
129
 
75
130
  @coverpage = options[:coverpage] || collection.coverpage
76
131
  @coverpage_pdf_portflio = options[:coverpage_pdf_portfolio] ||
@@ -90,21 +145,6 @@ module Metanorma
90
145
  directives_normalise_keystore_pdf_portfolio(directives)
91
146
  end
92
147
 
93
- # KILL
94
- def directives_normalise_coverpage_pdf_portfolio(directives)
95
- directives.reject! { |d| d.key == "coverpage-pdf-portfolio" }
96
- absolute_coverpage_pdf_portflio = @coverpage_pdf_portflio &&
97
- Pathname.new(@coverpage_pdf_portflio).absolute?
98
- @coverpage_pdf_portflio =
99
- Util::rel_path_resolve(@dirname, @coverpage_pdf_portflio)
100
- absolute_coverpage_pdf_portflio or
101
- @coverpage_pdf_portflio = Pathname.new(@coverpage_pdf_portflio)
102
- .relative_path_from(Pathname.new(@outdir)).to_s
103
- directives << ::Metanorma::Collection::Config::Directive
104
- .new(key: "coverpage-pdf-portfolio", value: @coverpage_pdf_portflio)
105
- directives
106
- end
107
-
108
148
  def directives_normalise_coverpage_pdf_portfolio(directives)
109
149
  directives_resolve_filepath(directives, "coverpage-pdf-portfolio",
110
150
  @coverpage_pdf_portflio)
@@ -258,10 +298,25 @@ module Metanorma
258
298
  .new(key: "documents-inline")
259
299
  out.bibdatas.each_key do |ident|
260
300
  id = @isodoc.docid_prefix(nil, ident.dup)
261
- @files.get(id, :attachment) || @files.get(id, :outputs).nil? and next
301
+ @files.get(id, :attachment) and next
302
+ # A non-attachment document with no compiled output for THIS format
303
+ # cannot be inlined: leaving it in place serialises as a bibdata-only,
304
+ # namespace-less <metanorma> that mn2pdf silently drops. Skip loudly
305
+ # rather than crash on a nil path or corrupt the collection PDF.
306
+ # (Sectionsplit parents get a :presentation output upstream, so they
307
+ # inline whole for the PDF/presentation path.)
308
+ outputs = @files.get(id, :outputs)
309
+ # The registered path may exist without the file: a member whose
310
+ # rendering failed registers its intended outputs but writes
311
+ # nothing (#586), so check the disk, not just the registration.
312
+ if outputs.nil? || outputs[ext].nil? || !File.exist?(outputs[ext])
313
+ warn "[metanorma] collection document '#{id}' has no compiled " \
314
+ "#{ext} output to inline; skipping (its doc-container would " \
315
+ "otherwise be bibdata-only)."
316
+ next
317
+ end
262
318
  out.documents[Util::key id] =
263
- Metanorma::Collection::Document
264
- .raw_file(@files.get(id, :outputs)[ext])
319
+ Metanorma::Collection::Document.raw_file(outputs[ext])
265
320
  end
266
321
  out
267
322
  end
@@ -7,8 +7,24 @@ require "concurrent-ruby"
7
7
 
8
8
  module Metanorma
9
9
  class Collection
10
+ # Sectionsplit is a collection render nested inside a collection render:
11
+ # a document is split into one output file per section, and those files are
12
+ # then rendered as their own (sub-)collection.
13
+ #
14
+ # Two output-location invariants matter (both load-bearing -- see the
15
+ # collection architecture review plan and metanorma/iso-10303#208):
16
+ #
17
+ # * XML section files are always written FLAT to the split directory
18
+ # (basename only). HTML output may instead carry a directory taken from
19
+ # sectionsplit_filename, reattached only at HTML compile time
20
+ # (preserve_directory_structure?).
21
+ # * A sectionsplit document's content is emitted at the sectionsplit output
22
+ # location (the collection root, unless sectionsplit_filename sets a
23
+ # directory), NOT at the document's own :out_path. Anything relativising a
24
+ # URL for such a document (e.g. attachment/citation links) must base it on
25
+ # the split location, or the ../../ overshoots.
10
26
  class Sectionsplit
11
- attr_accessor :filecache, :key
27
+ attr_accessor :filecache, :key, :whole_presentation_file
12
28
 
13
29
  def initialize(opts)
14
30
  @input_filename = opts[:input]
@@ -40,6 +56,13 @@ module Metanorma
40
56
  # Input XML is Semantic XML
41
57
  def sectionsplit
42
58
  xml = sectionsplit_prep(File.read(@input_filename), @base, @dir)
59
+ # Retain the whole (un-split) Presentation XML. sectionsplit is an
60
+ # HTML-only mechanism, but the collection PDF/presentation path must
61
+ # inline each document intact; without this the sectionsplit parent
62
+ # reaches concatenation with no presentation output and degrades to a
63
+ # bibdata-only, namespace-less <metanorma> stub that mn2pdf drops.
64
+ # https://github.com/metanorma/iso-10303/issues/208
65
+ @whole_presentation_file = write_whole_presentation(xml)
43
66
  @key = Metanorma::Collection::XrefProcess::xref_preprocess(xml, @isodoc)
44
67
  empty = empty_doc(xml)
45
68
  empty1 = empty_attachments(empty)
@@ -66,11 +89,11 @@ module Metanorma
66
89
 
67
90
  def sectionsplit2(xml, empty, chunks, parentnode, opt)
68
91
  @pool.post do
69
- output_filename = @sectionsplit_filename
70
- &.gsub(/\{document-num\}/, @parent_idx.to_s)
71
- &.gsub(/\{basename_legacy\}/, @base)
72
- &.gsub(/\{basename\}/, File.basename(@base, ".*"))
73
- &.gsub(/\{sectionsplit-num\}/, opt[:idx].to_s)
92
+ output_filename = Util.substitute_filename_pattern(
93
+ @sectionsplit_filename,
94
+ document_num: @parent_idx, basename: File.basename(@base, ".*"),
95
+ basename_legacy: @base, sectionsplit_num: opt[:idx]
96
+ )
74
97
  warn "Sectionsplit: #{output_filename}"
75
98
  a = sectionfile(xml, empty, output_filename, chunks,
76
99
  parentnode)
@@ -108,6 +131,16 @@ module Metanorma
108
131
  r
109
132
  end
110
133
 
134
+ # Persist the whole, svgmap-restored Presentation XML (the in-memory doc,
135
+ # not the on-disk tmp which still carries the internal svgmap1 marker) so
136
+ # the collection PDF path can inline the document whole. Written flat to
137
+ # the split output directory, where the per-section files also live.
138
+ def write_whole_presentation(xml)
139
+ fname = File.join(@splitdir, "#{@base}.whole.presentation.xml")
140
+ File.write(fname, xml.to_xml, encoding: "UTF-8")
141
+ fname
142
+ end
143
+
111
144
  def sectionsplit_preprocess_semxml(file, filename)
112
145
  xml = Nokogiri::XML(file, &:huge)
113
146
  type = xml.root["flavor"]
@@ -120,10 +153,8 @@ module Metanorma
120
153
 
121
154
  def sectionsplit_update_xrefs(xml)
122
155
  if c = @fileslookup&.parent
123
- n = c.nested
124
- c.nested = true # so unresolved erefs are not deleted
125
- c.update_xrefs(xml, @ident, {})
126
- c.nested = n
156
+ # nested mode so unresolved erefs are not deleted
157
+ c.with_nested { c.update_xrefs(xml, @ident, {}) }
127
158
  xml.xpath("//xmlns:svgmap").each { |x| x.name = "svgmap1" }
128
159
  # do not process svgmap until after files are split
129
160
  end
@@ -81,6 +81,22 @@ module Metanorma
81
81
  @c.decode(ident).gsub(/(\p{Zs})+/, " ")
82
82
  end
83
83
 
84
+ # Substitute special strings in filename patterns. Single source for
85
+ # both FileLookup#substitute_filename_pattern and sectionsplit.
86
+ # @param pattern [String] filename pattern with placeholders
87
+ # @param options [Hash] substitution values:
88
+ # :document_num, :basename, :basename_legacy, :sectionsplit_num
89
+ def substitute_filename_pattern(pattern, options = {})
90
+ pattern or return pattern
91
+ subs = { "{document-num}" => options[:document_num],
92
+ "{basename}" => options[:basename],
93
+ "{basename_legacy}" => options[:basename_legacy],
94
+ "{sectionsplit-num}" => options[:sectionsplit_num] }
95
+ subs.each_with_object(pattern.dup) do |(token, val), result|
96
+ val and result.gsub!(token, val.to_s)
97
+ end
98
+ end
99
+
84
100
  def taste2flavor(taste)
85
101
  tastes = Metanorma::TasteRegister.instance.aliases
86
102
  tastes[taste.to_sym] and taste = tastes[taste.to_sym]
@@ -135,8 +135,7 @@ module Metanorma
135
135
  output_paths[:out], ext, isodoc_options)
136
136
  wrap_html(options, output_paths[:ext], output_paths[:out])
137
137
  rescue StandardError => e
138
- strict = ext == :presentation || isodoc_options[:strict] == true
139
- isodoc_error_process(e, strict, false)
138
+ isodoc_error_process(e, strict_error?(ext, isodoc_options), false)
140
139
  end
141
140
 
142
141
  # Process format directly from semantic XML
@@ -145,8 +144,7 @@ module Metanorma
145
144
  ext, isodoc_options)
146
145
  true # Return as Thread equivalent
147
146
  rescue StandardError => e
148
- strict = ext == :presentation || isodoc_options[:strict] == "true"
149
- isodoc_error_process(e, strict, true)
147
+ isodoc_error_process(e, strict_error?(ext, isodoc_options), true)
150
148
  ext != :presentation
151
149
  end
152
150
 
@@ -168,12 +166,29 @@ module Metanorma
168
166
 
169
167
  private
170
168
 
169
+ # A presentation failure is always strict: every downstream format
170
+ # needs the presentation XML. The :strict option arrives as a
171
+ # boolean from the CLI but as a string from document attributes, so
172
+ # accept both (the two rescue sites previously each checked only
173
+ # one form).
174
+ def strict_error?(ext, isodoc_options)
175
+ ext == :presentation ||
176
+ [true, "true"].include?(isodoc_options[:strict])
177
+ end
178
+
179
+ # On the strict path the message is pooled into @errors for the
180
+ # caller to surface (the standalone CLI reports the pool and
181
+ # aborts; the collection renderer attributes it to the failed
182
+ # member, #586). The class+message line is also printed here in all
183
+ # cases, so it sits adjacent to its backtrace in the build log: a
184
+ # pooled message surfaces only at end of run, and a bare backtrace
185
+ # mid-log is undiagnosable. The exception class is retained because
186
+ # for errors like Encoding::UndefinedConversionError it carries
187
+ # half the diagnosis.
171
188
  def isodoc_error_process(err, strict, must_abort)
172
- if strict || err.message.include?("Fatal:")
173
- @errors << err.message
174
- else
175
- puts err.message
176
- end
189
+ msg = "#{err.class}: #{err.message}"
190
+ strict || err.message.include?("Fatal:") and @errors << msg
191
+ puts msg
177
192
  puts err.backtrace.join("\n")
178
193
  must_abort and 1
179
194
  end
@@ -1,3 +1,3 @@
1
1
  module Metanorma
2
- VERSION = "2.4.3".freeze
2
+ VERSION = "2.5.2".freeze
3
3
  end
data/metanorma.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.bindir = "bin"
23
23
  # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
25
- spec.required_ruby_version = ">= 3.1.0"
25
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
26
26
 
27
27
  spec.add_runtime_dependency "asciidoctor"
28
28
  spec.add_runtime_dependency "concurrent-ruby"