metanorma 2.0.0 → 2.5.5

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +23 -2
  5. data/Gemfile +17 -0
  6. data/Gemfile.devel +10 -0
  7. data/README.adoc +1 -1
  8. data/lib/metanorma/collection/artifact_store.rb +142 -0
  9. data/lib/metanorma/collection/collection.rb +160 -122
  10. data/lib/metanorma/collection/config/bibdata.rb +3 -3
  11. data/lib/metanorma/collection/config/compile_options.rb +4 -5
  12. data/lib/metanorma/collection/config/config.rb +81 -83
  13. data/lib/metanorma/collection/config/converters.rb +92 -8
  14. data/lib/metanorma/collection/config/directive.rb +9 -3
  15. data/lib/metanorma/collection/config/doc_container.rb +28 -0
  16. data/lib/metanorma/collection/config/manifest.rb +59 -29
  17. data/lib/metanorma/collection/config/namespaces.rb +12 -0
  18. data/lib/metanorma/collection/document/document.rb +56 -15
  19. data/lib/metanorma/collection/filelookup/filelookup.rb +185 -77
  20. data/lib/metanorma/collection/filelookup/filelookup_sectionsplit.rb +99 -27
  21. data/lib/metanorma/collection/filelookup/utils.rb +52 -0
  22. data/lib/metanorma/collection/helpers.rb +82 -0
  23. data/lib/metanorma/collection/log.rb +24 -0
  24. data/lib/metanorma/collection/manifest/manifest.rb +45 -14
  25. data/lib/metanorma/collection/multilingual/multilingual.rb +660 -0
  26. data/lib/metanorma/collection/renderer/filelocation.rb +166 -0
  27. data/lib/metanorma/collection/renderer/fileparse.rb +135 -115
  28. data/lib/metanorma/collection/renderer/fileprocess.rb +185 -37
  29. data/lib/metanorma/collection/renderer/navigation.rb +21 -7
  30. data/lib/metanorma/collection/renderer/render_word.rb +15 -10
  31. data/lib/metanorma/collection/renderer/renderer.rb +236 -32
  32. data/lib/metanorma/collection/renderer/svg.rb +107 -0
  33. data/lib/metanorma/collection/renderer/utils.rb +110 -39
  34. data/lib/metanorma/collection/sectionsplit/collection.rb +110 -0
  35. data/lib/metanorma/collection/sectionsplit/sectionsplit.rb +198 -115
  36. data/lib/metanorma/collection/util/disambig_files.rb +4 -5
  37. data/lib/metanorma/collection/util/util.rb +155 -18
  38. data/lib/metanorma/collection/xrefprocess/xrefprocess.rb +47 -33
  39. data/lib/metanorma/compile/assets/icc-boilerplate.adoc +41 -0
  40. data/lib/metanorma/compile/compile.rb +181 -156
  41. data/lib/metanorma/compile/compile_options.rb +186 -79
  42. data/lib/metanorma/compile/extract.rb +75 -58
  43. data/lib/metanorma/compile/flavor.rb +28 -0
  44. data/lib/metanorma/compile/output_filename.rb +75 -0
  45. data/lib/metanorma/compile/output_filename_config.rb +27 -0
  46. data/lib/metanorma/compile/relaton_drop.rb +56 -0
  47. data/lib/metanorma/compile/render.rb +226 -0
  48. data/lib/metanorma/compile/validator.rb +26 -0
  49. data/lib/metanorma/compile/writeable.rb +12 -0
  50. data/lib/metanorma/util/fontist_helper.rb +35 -19
  51. data/lib/metanorma/version.rb +1 -1
  52. data/lib/metanorma.rb +1 -7
  53. data/metanorma.gemspec +15 -21
  54. data/plans/collection-rendering-architecture-review.md +516 -0
  55. metadata +65 -113
  56. data/.hound.yml +0 -5
  57. data/lib/metanorma/asciidoctor_extensions/glob_include_processor.rb +0 -22
  58. data/lib/metanorma/asciidoctor_extensions.rb +0 -1
  59. data/lib/metanorma/compile/compile_validate.rb +0 -68
  60. data/lib/metanorma/config/config.rb +0 -23
  61. data/lib/metanorma/input/asciidoc.rb +0 -107
  62. data/lib/metanorma/input/base.rb +0 -9
  63. data/lib/metanorma/input.rb +0 -7
  64. data/lib/metanorma/processor/processor.rb +0 -54
  65. data/lib/metanorma/processor.rb +0 -6
  66. data/lib/metanorma/registry/registry.rb +0 -63
  67. data/lib/metanorma/shale_monkeypatch.rb +0 -15
  68. data/lib/metanorma/util/util.rb +0 -45
@@ -1,21 +1,51 @@
1
1
  require "isodoc"
2
2
  require "htmlentities"
3
+ require "mime/types"
3
4
  require_relative "fileprocess"
4
5
  require_relative "../../util/fontist_helper"
5
- require_relative "../../util/util"
6
+ require "metanorma-core"
6
7
  require_relative "../filelookup/filelookup"
8
+ require_relative "../multilingual/multilingual"
7
9
  require_relative "utils"
8
10
  require_relative "render_word"
9
11
  require_relative "navigation"
12
+ require_relative "svg"
13
+ require_relative "../log"
10
14
 
11
15
  module Metanorma
12
16
  class Collection
13
17
  class Renderer
14
- FORMATS = %i[html xml doc pdf].freeze
18
+ FORMATS = %i[html xml doc pdf pdf-portfolio].freeze
15
19
 
16
- attr_accessor :isodoc, :nested
20
+ attr_accessor :isodoc, :isodoc_presxml
17
21
  attr_reader :xml, :compile, :compile_options, :documents, :outdir,
18
- :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
19
49
 
20
50
  # This is only going to render the HTML collection
21
51
  # @param xml [Metanorma::Collection] input XML collection
@@ -32,21 +62,26 @@ module Metanorma
32
62
  def initialize(collection, folder, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
33
63
  check_options options
34
64
  @xml = Nokogiri::XML collection.to_xml # @xml is the collection manifest
35
- @xml.root.default_namespace = "http://metanorma.org"
36
- @lang = collection.bibdata.language.first || "en"
37
- @script = collection.bibdata.script.first || "Latn"
65
+ @lang = Array(collection.bibdata.language).first || "en"
66
+ @script = Array(collection.bibdata.script).first || "Latn"
38
67
  @locale = @xml.at("//xmlns:bibdata/xmlns:locale")&.text
39
- @doctype = doctype
68
+ @registry = Metanorma::Registry.instance
69
+ @flavor = options[:flavor] || flavor
40
70
  @compile = Compile.new
41
- @compile.load_flavor(@doctype)
71
+ @compile.load_flavor(@flavor)
42
72
 
43
- @isodoc = isodoc_create # output processor for flavour
73
+ # output processor for flavour
74
+ @isodoc = Util::isodoc_create(Util::taste2flavor(@flavor), @lang,
75
+ @script, @xml)
76
+ @isodoc_presxml = Util::isodoc_create(Util::taste2flavor(@flavor),
77
+ @lang, @script, @xml,
78
+ presxml: true)
44
79
  @outdir = dir_name_cleanse(options[:output_folder])
45
- @coverpage = options[:coverpage] || collection.coverpage
46
80
  @format = ::Metanorma::Util.sort_extensions_execution(options[:format])
47
81
  @compile_options = options[:compile] || {}
48
82
  @compile_options[:install_fonts] = true if options[:install_fonts]
49
83
  @log = options[:log]
84
+ @log&.add_msg(METANORMA_LOG_MESSAGES)
50
85
  @bibdata = collection.bibdata
51
86
  @documents = collection.documents
52
87
  @bibdatas = collection.documents
@@ -58,10 +93,45 @@ module Metanorma
58
93
  @final = collection.final
59
94
  @c = HTMLEntities.new
60
95
  @files_to_delete = []
61
- @nested = options[:nested] # if false, this is the root instance of Renderer
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 = []
100
+ @nested = options[:nested]
101
+ # if false, this is the root instance of Renderer
62
102
  # if true, then this is not the last time Renderer will be run
63
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]
64
129
 
130
+ @coverpage = options[:coverpage] || collection.coverpage
131
+ @coverpage_pdf_portflio = options[:coverpage_pdf_portfolio] ||
132
+ collection.coverpage_pdf_portfolio || Util::taste2coverpage_pdf_portfolio(@flavor)
133
+ collection.directives = directives_normalise(collection.directives)
134
+ @directives = collection.directives
65
135
  # list of files in the collection
66
136
  @files = Metanorma::Collection::FileLookup.new(folder, self)
67
137
  @files.add_section_split
@@ -69,8 +139,57 @@ module Metanorma
69
139
  create_non_existing_directory(@outdir)
70
140
  end
71
141
 
142
+ def directives_normalise(directives)
143
+ @coverpage_pdf_portflio and
144
+ directives = directives_normalise_coverpage_pdf_portfolio(directives)
145
+ directives_normalise_keystore_pdf_portfolio(directives)
146
+ end
147
+
148
+ def directives_normalise_coverpage_pdf_portfolio(directives)
149
+ directives_resolve_filepath(directives, "coverpage-pdf-portfolio",
150
+ @coverpage_pdf_portflio)
151
+ end
152
+
153
+ def directives_normalise_keystore_pdf_portfolio(directives)
154
+ f = directives.find { |d| d.key == "keystore-pdf-portfolio" }
155
+ f.nil? || f.value.nil? and return directives
156
+ directives_resolve_filepath(directives, "keystore-pdf-portfolio",
157
+ f.value)
158
+ end
159
+
160
+ def directives_resolve_filepath(directives, name, val)
161
+ abs = Pathname.new(val).absolute?
162
+ directives.reject! { |d| d.key == name }
163
+ val = Util::rel_path_resolve(@dirname, val)
164
+ val =
165
+ if abs then directives_host_materialise(val)
166
+ else Pathname.new(val).relative_path_from(Pathname.new(@outdir)).to_s
167
+ end
168
+ directives << ::Metanorma::Collection::Config::Directive
169
+ .new(key: name, value: val)
170
+ directives
171
+ end
172
+
173
+ # An absolute directive value can point inside a gem payload (e.g. a
174
+ # taste's data/ file from Util.taste2coverpage_pdf_portfolio). A
175
+ # spawned child process that is not this Ruby (mn2pdf/java) may be
176
+ # unable to read such a path (a packaged virtual filesystem mount,
177
+ # e.g. tebako on Windows). Copy the file into the output folder —
178
+ # the child already reads the collection XML from there — and point
179
+ # the directive at the copy. A missing file keeps its value: the
180
+ # child reports that as it always has.
181
+ def directives_host_materialise(val)
182
+ File.exist?(val) or return val
183
+ dst = File.expand_path(File.join(@outdir, File.basename(val)))
184
+ dst == File.expand_path(val) and return val
185
+ FileUtils.mkdir_p(@outdir)
186
+ FileUtils.cp(val, dst)
187
+ dst
188
+ end
189
+
72
190
  def flush_files
73
191
  warn "\n\n\n\n\nDone: #{DateTime.now.strftime('%H:%M:%S')}"
192
+ warn "\nFiles to delete:\n"
74
193
  warn @files.files_to_delete
75
194
  @files.files_to_delete.each { |f| FileUtils.rm_f(f) }
76
195
  @files_to_delete.each { |f| FileUtils.rm_f(f) }
@@ -85,61 +204,145 @@ module Metanorma
85
204
  warn "\n\n\n\n\nRender Init: #{DateTime.now.strftime('%H:%M:%S')}"
86
205
  cr = new(col, File.dirname(col.file), options)
87
206
  cr.files
207
+ cr.rxl(options)
88
208
  cr.concatenate(col, options)
89
209
  options[:format]&.include?(:html) and cr.coverpage
90
210
  cr.flush_files
91
211
  cr
92
212
  end
93
213
 
214
+ def rxl(options)
215
+ @bibdata or return
216
+ options[:site_generate] and options[:format] << :xml
217
+ options[:format].include?(:rxl) or return
218
+ File.open(File.join(@outdir, "collection.rxl"), "w:UTF-8") do |f|
219
+ f.write(@bibdata.to_xml(bibdata: true))
220
+ end
221
+ end
222
+
94
223
  def concatenate(col, options)
95
224
  warn "\n\n\n\n\nConcatenate: #{DateTime.now.strftime('%H:%M:%S')}"
96
- (options[:format] & %i(pdf doc)).empty? or
225
+ concatenate_presentation?(options) and
97
226
  options[:format] << :presentation
98
227
  concatenate_prep(col, options)
99
228
  concatenate_outputs(options)
100
229
  end
101
230
 
231
+ def concatenate_presentation?(options)
232
+ !(options[:format] & %i(pdf doc)).empty? ||
233
+ (@directives.detect { |d| d.key == "bilingual" } &&
234
+ options[:format].include?(:html))
235
+ end
236
+
102
237
  def concatenate_prep(col, options)
238
+ warn options[:format]
103
239
  %i(xml presentation).each do |e|
104
240
  options[:format].include?(e) or next
105
241
  ext = e == :presentation ? "presentation.xml" : e.to_s
106
242
  File.open(File.join(@outdir, "collection.#{ext}"), "w:UTF-8") do |f|
107
243
  b = concatenate1(col.clone, e).to_xml
108
- e == :presentation and
109
- b.sub!("<metanorma-collection>", "<metanorma-collection xmlns='http://metanorma.org'>")
110
- # TODO BEING FORCED TO DO THAT BECAUSE SHALE IS NOT DEALING WITH DEFAULT NAMESPACES
244
+ e == :presentation and b = concatenate_presentation(b)
111
245
  f.write(b)
112
246
  end
113
247
  end
114
248
  end
115
249
 
250
+ def concatenate_presentation(xml)
251
+ @directives.detect { |d| d.key == "bilingual" } and
252
+ xml = Metanorma::Collection::Multilingual
253
+ .new({ align_cross_elements: %w(p note) }).to_bilingual(xml)
254
+ xml
255
+ end
256
+
116
257
  def concatenate_outputs(options)
117
- pres = File.join(@outdir, "collection.presentation.xml")
118
- options[:format].include?(:pdf) and pdfconv.convert(pres)
258
+ pres, compile_opts = concatenate_outputs_prep(options)
259
+ warn pp compile_opts
260
+ options[:format].include?(:pdf) and pdfconv(compile_opts).convert(pres)
261
+ options[:format].include?(:"pdf-portfolio") and
262
+ pdfconv(pdf_portfolio_mn2pdf_options)
263
+ .convert(pres, nil, nil,
264
+ File.join(@outdir, "collection.portfolio.pdf"))
119
265
  options[:format].include?(:doc) and docconv_convert(pres)
266
+ bilingual_output(options, pres)
267
+ end
268
+
269
+ def concatenate_outputs_prep(_options)
270
+ pres = File.join(@outdir, "collection.presentation.xml")
271
+ fonts = extract_added_fonts(pres)
272
+ if fonts
273
+ # Install fonts before trying to locate them
274
+ font_options = @compile_options.merge({ fonts: fonts })
275
+ ::Metanorma::Util::FontistHelper.install_fonts(@compile.processor,
276
+ font_options)
277
+
278
+ mn2pdf = {
279
+ font_manifest: ::Metanorma::Util::FontistHelper
280
+ .location_manifest(@compile.processor, { fonts: fonts }),
281
+ }
282
+ end
283
+ [pres, { fonts: fonts, mn2pdf: mn2pdf }.compact]
284
+ end
285
+
286
+ def extract_added_fonts(pres)
287
+ File.exist?(pres) or return
288
+ xml = Nokogiri::XML(File.read(pres, encoding: "UTF-8"), &:huge)
289
+ x = xml.xpath("//*[local-name() = 'presentation-metadata']/" \
290
+ "*[local-name() = 'fonts']")
291
+ x.empty? and return
292
+ x.map(&:text).join(";")
293
+ end
294
+
295
+ def pdf_portfolio_mn2pdf_options
296
+ f1 = @directives.find { |d| d.key == "keystore-pdf-portfolio" }&.value
297
+ f2 = @directives.find do |d|
298
+ d.key == "keystore-password-pdf-portfolio"
299
+ end&.value
300
+ { "pdf-portfolio": "true", pdfkeystore: f1,
301
+ pdfkeystorepassword: f2 }.compact
302
+ end
303
+
304
+ def bilingual_output(options, pres)
305
+ @directives.detect { |d| d.key == "bilingual" } &&
306
+ options[:format].include?(:html) and
307
+ Metanorma::Collection::Multilingual.new(
308
+ { flavor: Util::taste2flavor(flavor).to_sym,
309
+ converter_options: PdfOptionsNode.new(Util::taste2flavor(flavor),
310
+ @compile_options),
311
+ outdir: @outdir },
312
+ ).to_html(pres)
120
313
  end
121
314
 
122
315
  def concatenate1(out, ext)
123
- out.directives << ::Metanorma::Collection::Config::Directive.new(key: "documents-inline")
316
+ out.directives << ::Metanorma::Collection::Config::Directive
317
+ .new(key: "documents-inline")
124
318
  out.bibdatas.each_key do |ident|
125
319
  id = @isodoc.docid_prefix(nil, ident.dup)
126
- @files.get(id, :attachment) || @files.get(id, :outputs).nil? and next
320
+ @files.get(id, :attachment) and next
321
+ # A non-attachment document with no compiled output for THIS format
322
+ # cannot be inlined: leaving it in place serialises as a bibdata-only,
323
+ # namespace-less <metanorma> that mn2pdf silently drops. Skip loudly
324
+ # rather than crash on a nil path or corrupt the collection PDF.
325
+ # (Sectionsplit parents get a :presentation output upstream, so they
326
+ # inline whole for the PDF/presentation path.)
327
+ outputs = @files.get(id, :outputs)
328
+ # The registered path may exist without the file: a member whose
329
+ # rendering failed registers its intended outputs but writes
330
+ # nothing (#586), so check the disk, not just the registration.
331
+ if outputs.nil? || outputs[ext].nil? || !File.exist?(outputs[ext])
332
+ warn "[metanorma] collection document '#{id}' has no compiled " \
333
+ "#{ext} output to inline; skipping (its doc-container would " \
334
+ "otherwise be bibdata-only)."
335
+ next
336
+ end
127
337
  out.documents[Util::key id] =
128
- Metanorma::Collection::Document.raw_file(@files.get(id,
129
- :outputs)[ext])
338
+ Metanorma::Collection::Document.raw_file(outputs[ext])
130
339
  end
131
340
  out
132
341
  end
133
342
 
134
- # infer the flavour from the first document identifier; relaton does that
135
- def doctype
136
- if (docid = @xml.at("//bibdata/docidentifier/@type")&.text)
137
- dt = docid.downcase
138
- elsif (docid = @xml.at("//bibdata/docidentifier")&.text)
139
- dt = docid.sub(/\s.*$/, "").lowercase
140
- else return "standoc"
141
- end
142
- @registry = Metanorma::Registry.instance
343
+ # TODO: infer flavor from publisher when available
344
+ def flavor
345
+ dt = @xml.at("//bibdata/ext/flavor")&.text or return "standoc"
143
346
  @registry.alias(dt.to_sym)&.to_s || dt
144
347
  end
145
348
 
@@ -147,9 +350,10 @@ module Metanorma
147
350
  # collection manifest
148
351
  def coverpage
149
352
  @coverpage or return
353
+ @coverpage_path = Util::rel_path_resolve(@dirname, @coverpage)
150
354
  warn "\n\n\n\n\nCoverpage: #{DateTime.now.strftime('%H:%M:%S')}"
151
355
  File.open(File.join(@outdir, "index.html"), "w:UTF-8") do |f|
152
- f.write @isodoc.populate_template(File.read(@coverpage))
356
+ f.write @isodoc.populate_template(File.read(@coverpage_path))
153
357
  end
154
358
  end
155
359
  end
@@ -0,0 +1,107 @@
1
+ module Metanorma
2
+ class Collection
3
+ class Renderer
4
+ # Converts SVG images to data URIs for inline embedding
5
+ def svg_datauri(docxml, docid)
6
+ rel = @files.get(docid, :rel_path)
7
+ parent = @files.get(docid, :parentid) and
8
+ rel = @files.get(parent, :rel_path)
9
+ # if sectionsplit, use orig file dir
10
+ dir = File.join(@dirname, File.dirname(rel))
11
+ datauri_encode(docxml, dir)
12
+ end
13
+
14
+ # Resolves SVG map references and processes SVG ID disambiguation.
15
+ #
16
+ # Delegates SVG manipulation to Vectory gem, which handles:
17
+ # 1. Document-level suffixing for cross-document ID disambiguation
18
+ # 2. Index-based suffixing for multiple svgmaps within one document
19
+ # 3. Link remapping and SVG extraction
20
+ #
21
+ # Metanorma's role: State requirements and coordinate the workflow.
22
+ # Vectory's role: Handle all SVG ID manipulation internally.
23
+ #
24
+ # @param docxml [Nokogiri::XML::Document] The document XML to process
25
+ # @param docid [String] Document identifier for retrieving suffix
26
+ # @param presxml [Boolean, nil] Whether this is presentation XML
27
+ def svgmap_resolve(docxml, docid, presxml)
28
+ ids, docxml, isodoc, tag = svgmap_resolve_prep(docxml, docid, presxml)
29
+
30
+ # Stage 1: Resolve EREF references to their targets
31
+ resolve_svgmap_erefs(docxml, tag, isodoc, ids, presxml)
32
+
33
+ # Stage 2: Normalize prefixes (Vectory expects eref, not fmt-eref)
34
+ normalize_svgmap_prefixes(docxml)
35
+
36
+ # Stage 3: Process with Vectory
37
+ # Pass document suffix to Vectory as id_suffix for proper SVG ID disambiguation.
38
+ # Vectory handles both id_suffix (document-level) and index suffix internally.
39
+ doc_suffix = @files.get(docid, :document_suffix)
40
+ Vectory::SvgMapping.new(docxml, "", id_suffix: doc_suffix).call
41
+
42
+ # Stage 4: Extract processed SVG content
43
+ extract_svgmap_content(docxml, isodoc)
44
+ end
45
+
46
+ # Prepares document and context for svgmap resolution
47
+ def svgmap_resolve_prep(docxml, docid, presxml)
48
+ ids = @files.get(docid, :ids)
49
+ docxml = svg_unnest(svg_datauri(docxml, docid))
50
+ isodoc = IsoDoc::PresentationXMLConvert.new({})
51
+ isodoc.bibitem_lookup(docxml)
52
+ tag = presxml ? "fmt-eref" : "eref"
53
+ [ids, docxml, isodoc, tag]
54
+ end
55
+
56
+ private
57
+
58
+ # Resolves EREF elements within svgmap to their actual link targets
59
+ def resolve_svgmap_erefs(docxml, tag, isodoc, ids, presxml)
60
+ docxml.xpath(ns("//svgmap//#{tag}")).each do |e|
61
+ svgmap_resolve_eref(e, isodoc, docxml, ids, presxml)
62
+ end
63
+ end
64
+
65
+ # Converts fmt-eref/fmt-link/fmt-xref back to eref/link/xref
66
+ # Vectory expects non-prefixed element names for proper processing
67
+ def normalize_svgmap_prefixes(docxml)
68
+ svgmap_fmt_prefix_remove(docxml)
69
+ end
70
+
71
+ # Extracts processed SVG content from svgmap elements
72
+ def extract_svgmap_content(docxml, isodoc)
73
+ docxml.xpath(ns("//svgmap")).each { |s| isodoc.svgmap_extract(s) }
74
+ end
75
+
76
+ # Converts Presentation XML fmt- prefix back to standard element names
77
+ # Vectory expects eref/link/xref, not fmt-eref/fmt-link/fmt-xref
78
+ def svgmap_fmt_prefix_remove(docxml)
79
+ docxml.xpath(ns("//svgmap/target")).each do |t|
80
+ n = t.at(ns(".//fmt-link | .//fmt-xref | .//fmt-eref")) or next
81
+ n.name = n.name.sub(/^fmt-/, "")
82
+ t.children = n
83
+ end
84
+ end
85
+
86
+ # Removes nested image elements within svgmap, flattening the structure
87
+ def svg_unnest(docxml)
88
+ docxml.xpath(ns("//svgmap//image[.//*[name() = 'image']]")).each do |i|
89
+ s = i.elements.detect { |e| e.name == "svg" } and
90
+ i.replace(s)
91
+ end
92
+ docxml
93
+ end
94
+
95
+ # Resolves a single EREF element to its target link
96
+ def svgmap_resolve_eref(eref, isodoc, _docxml, ids, presxml)
97
+ href = isodoc.eref_target(eref) or return
98
+ href = href[:link]
99
+ href == "##{eref['bibitemid']}" ||
100
+ (href =~ /^#/ && !ids[href.sub(/^#/, "")]) and return
101
+ eref["target"] = href.strip
102
+ eref.name = presxml ? "fmt-link" : "link"
103
+ eref.elements&.remove
104
+ end
105
+ end
106
+ end
107
+ end