metanorma 2.1.4 → 2.5.4

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +23 -2
  3. data/Gemfile +14 -0
  4. data/README.adoc +1 -1
  5. data/lib/metanorma/collection/artifact_store.rb +142 -0
  6. data/lib/metanorma/collection/collection.rb +140 -131
  7. data/lib/metanorma/collection/config/bibdata.rb +1 -1
  8. data/lib/metanorma/collection/config/config.rb +50 -20
  9. data/lib/metanorma/collection/config/converters.rb +89 -31
  10. data/lib/metanorma/collection/config/doc_container.rb +28 -0
  11. data/lib/metanorma/collection/config/manifest.rb +30 -6
  12. data/lib/metanorma/collection/config/namespaces.rb +12 -0
  13. data/lib/metanorma/collection/document/document.rb +54 -15
  14. data/lib/metanorma/collection/filelookup/filelookup.rb +170 -74
  15. data/lib/metanorma/collection/filelookup/filelookup_sectionsplit.rb +62 -27
  16. data/lib/metanorma/collection/filelookup/utils.rb +52 -0
  17. data/lib/metanorma/collection/helpers.rb +82 -0
  18. data/lib/metanorma/collection/log.rb +24 -0
  19. data/lib/metanorma/collection/manifest/manifest.rb +27 -14
  20. data/lib/metanorma/collection/multilingual/multilingual.rb +1 -1
  21. data/lib/metanorma/collection/renderer/filelocation.rb +166 -0
  22. data/lib/metanorma/collection/renderer/fileparse.rb +60 -55
  23. data/lib/metanorma/collection/renderer/fileprocess.rb +184 -40
  24. data/lib/metanorma/collection/renderer/navigation.rb +20 -6
  25. data/lib/metanorma/collection/renderer/render_word.rb +8 -4
  26. data/lib/metanorma/collection/renderer/renderer.rb +202 -19
  27. data/lib/metanorma/collection/renderer/svg.rb +60 -5
  28. data/lib/metanorma/collection/renderer/utils.rb +76 -42
  29. data/lib/metanorma/collection/sectionsplit/collection.rb +14 -4
  30. data/lib/metanorma/collection/sectionsplit/sectionsplit.rb +141 -61
  31. data/lib/metanorma/collection/util/disambig_files.rb +4 -5
  32. data/lib/metanorma/collection/util/util.rb +147 -23
  33. data/lib/metanorma/collection/xrefprocess/xrefprocess.rb +7 -7
  34. data/lib/metanorma/compile/assets/icc-boilerplate.adoc +41 -0
  35. data/lib/metanorma/compile/compile.rb +180 -165
  36. data/lib/metanorma/compile/compile_options.rb +155 -83
  37. data/lib/metanorma/compile/extract.rb +76 -56
  38. data/lib/metanorma/compile/flavor.rb +19 -0
  39. data/lib/metanorma/compile/output_filename.rb +75 -0
  40. data/lib/metanorma/compile/output_filename_config.rb +27 -0
  41. data/lib/metanorma/compile/relaton_drop.rb +56 -0
  42. data/lib/metanorma/compile/render.rb +196 -0
  43. data/lib/metanorma/compile/validator.rb +26 -0
  44. data/lib/metanorma/compile/writeable.rb +12 -0
  45. data/lib/metanorma/util/fontist_helper.rb +35 -19
  46. data/lib/metanorma/version.rb +1 -1
  47. data/lib/metanorma.rb +1 -7
  48. data/metanorma.gemspec +11 -20
  49. data/plans/collection-rendering-architecture-review.md +516 -0
  50. metadata +42 -122
  51. data/.hound.yml +0 -5
  52. data/lib/metanorma/asciidoctor_extensions/glob_include_processor.rb +0 -22
  53. data/lib/metanorma/asciidoctor_extensions.rb +0 -1
  54. data/lib/metanorma/compile/compile_validate.rb +0 -68
  55. data/lib/metanorma/config/config.rb +0 -23
  56. data/lib/metanorma/input/asciidoc.rb +0 -88
  57. data/lib/metanorma/input/base.rb +0 -9
  58. data/lib/metanorma/input.rb +0 -7
  59. data/lib/metanorma/processor/processor.rb +0 -54
  60. data/lib/metanorma/processor.rb +0 -6
  61. data/lib/metanorma/registry/registry.rb +0 -63
  62. data/lib/metanorma/util/util.rb +0 -45
@@ -1,23 +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"
7
8
  require_relative "../multilingual/multilingual"
8
9
  require_relative "utils"
9
10
  require_relative "render_word"
10
11
  require_relative "navigation"
11
12
  require_relative "svg"
13
+ require_relative "../log"
12
14
 
13
15
  module Metanorma
14
16
  class Collection
15
17
  class Renderer
16
- FORMATS = %i[html xml doc pdf].freeze
18
+ FORMATS = %i[html xml doc pdf pdf-portfolio].freeze
17
19
 
18
- attr_accessor :isodoc, :nested
20
+ attr_accessor :isodoc, :isodoc_presxml
19
21
  attr_reader :xml, :compile, :compile_options, :documents, :outdir,
20
- :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
21
49
 
22
50
  # This is only going to render the HTML collection
23
51
  # @param xml [Metanorma::Collection] input XML collection
@@ -34,9 +62,8 @@ module Metanorma
34
62
  def initialize(collection, folder, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
35
63
  check_options options
36
64
  @xml = Nokogiri::XML collection.to_xml # @xml is the collection manifest
37
- @xml.root.default_namespace = "http://metanorma.org"
38
- @lang = collection.bibdata.language.first || "en"
39
- @script = collection.bibdata.script.first || "Latn"
65
+ @lang = Array(collection.bibdata.language).first || "en"
66
+ @script = Array(collection.bibdata.script).first || "Latn"
40
67
  @locale = @xml.at("//xmlns:bibdata/xmlns:locale")&.text
41
68
  @registry = Metanorma::Registry.instance
42
69
  @flavor = options[:flavor] || flavor
@@ -44,13 +71,17 @@ module Metanorma
44
71
  @compile.load_flavor(@flavor)
45
72
 
46
73
  # output processor for flavour
47
- @isodoc = Util::isodoc_create(@flavor, @lang, @script, @xml)
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)
48
79
  @outdir = dir_name_cleanse(options[:output_folder])
49
- @coverpage = options[:coverpage] || collection.coverpage
50
80
  @format = ::Metanorma::Util.sort_extensions_execution(options[:format])
51
81
  @compile_options = options[:compile] || {}
52
82
  @compile_options[:install_fonts] = true if options[:install_fonts]
53
83
  @log = options[:log]
84
+ @log&.add_msg(METANORMA_LOG_MESSAGES)
54
85
  @bibdata = collection.bibdata
55
86
  @documents = collection.documents
56
87
  @bibdatas = collection.documents
@@ -62,11 +93,45 @@ module Metanorma
62
93
  @final = collection.final
63
94
  @c = HTMLEntities.new
64
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 = []
65
100
  @nested = options[:nested]
66
101
  # if false, this is the root instance of Renderer
67
102
  # if true, then this is not the last time Renderer will be run
68
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]
69
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
70
135
  # list of files in the collection
71
136
  @files = Metanorma::Collection::FileLookup.new(folder, self)
72
137
  @files.add_section_split
@@ -74,6 +139,54 @@ module Metanorma
74
139
  create_non_existing_directory(@outdir)
75
140
  end
76
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
+
77
190
  def flush_files
78
191
  warn "\n\n\n\n\nDone: #{DateTime.now.strftime('%H:%M:%S')}"
79
192
  warn "\nFiles to delete:\n"
@@ -91,12 +204,22 @@ module Metanorma
91
204
  warn "\n\n\n\n\nRender Init: #{DateTime.now.strftime('%H:%M:%S')}"
92
205
  cr = new(col, File.dirname(col.file), options)
93
206
  cr.files
207
+ cr.rxl(options)
94
208
  cr.concatenate(col, options)
95
209
  options[:format]&.include?(:html) and cr.coverpage
96
210
  cr.flush_files
97
211
  cr
98
212
  end
99
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
+
100
223
  def concatenate(col, options)
101
224
  warn "\n\n\n\n\nConcatenate: #{DateTime.now.strftime('%H:%M:%S')}"
102
225
  concatenate_presentation?(options) and
@@ -112,6 +235,7 @@ module Metanorma
112
235
  end
113
236
 
114
237
  def concatenate_prep(col, options)
238
+ warn options[:format]
115
239
  %i(xml presentation).each do |e|
116
240
  options[:format].include?(e) or next
117
241
  ext = e == :presentation ? "presentation.xml" : e.to_s
@@ -124,8 +248,6 @@ module Metanorma
124
248
  end
125
249
 
126
250
  def concatenate_presentation(xml)
127
- xml.sub!("<metanorma-collection>", "<metanorma-collection xmlns='http://metanorma.org'>")
128
- # TODO BEING FORCED TO DO THAT BECAUSE SHALE IS NOT DEALING WITH DEFAULT NAMESPACES
129
251
  @directives.detect { |d| d.key == "bilingual" } and
130
252
  xml = Metanorma::Collection::Multilingual
131
253
  .new({ align_cross_elements: %w(p note) }).to_bilingual(xml)
@@ -133,14 +255,59 @@ module Metanorma
133
255
  end
134
256
 
135
257
  def concatenate_outputs(options)
136
- pres = File.join(@outdir, "collection.presentation.xml")
137
- 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"))
138
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)
139
305
  @directives.detect { |d| d.key == "bilingual" } &&
140
306
  options[:format].include?(:html) and
141
307
  Metanorma::Collection::Multilingual.new(
142
- { flavor: flavor.to_sym,
143
- converter_options: PdfOptionsNode.new(flavor, @compile_options),
308
+ { flavor: Util::taste2flavor(flavor).to_sym,
309
+ converter_options: PdfOptionsNode.new(Util::taste2flavor(flavor),
310
+ @compile_options),
144
311
  outdir: @outdir },
145
312
  ).to_html(pres)
146
313
  end
@@ -150,10 +317,25 @@ module Metanorma
150
317
  .new(key: "documents-inline")
151
318
  out.bibdatas.each_key do |ident|
152
319
  id = @isodoc.docid_prefix(nil, ident.dup)
153
- @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
154
337
  out.documents[Util::key id] =
155
- Metanorma::Collection::Document
156
- .raw_file(@files.get(id, :outputs)[ext])
338
+ Metanorma::Collection::Document.raw_file(outputs[ext])
157
339
  end
158
340
  out
159
341
  end
@@ -168,9 +350,10 @@ module Metanorma
168
350
  # collection manifest
169
351
  def coverpage
170
352
  @coverpage or return
353
+ @coverpage_path = Util::rel_path_resolve(@dirname, @coverpage)
171
354
  warn "\n\n\n\n\nCoverpage: #{DateTime.now.strftime('%H:%M:%S')}"
172
355
  File.open(File.join(@outdir, "index.html"), "w:UTF-8") do |f|
173
- f.write @isodoc.populate_template(File.read(@coverpage))
356
+ f.write @isodoc.populate_template(File.read(@coverpage_path))
174
357
  end
175
358
  end
176
359
  end
@@ -1,6 +1,7 @@
1
1
  module Metanorma
2
2
  class Collection
3
3
  class Renderer
4
+ # Converts SVG images to data URIs for inline embedding
4
5
  def svg_datauri(docxml, docid)
5
6
  rel = @files.get(docid, :rel_path)
6
7
  parent = @files.get(docid, :parentid) and
@@ -10,26 +11,79 @@ module Metanorma
10
11
  datauri_encode(docxml, dir)
11
12
  end
12
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
13
27
  def svgmap_resolve(docxml, docid, presxml)
14
- #require "debug"; binding.b
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)
15
48
  ids = @files.get(docid, :ids)
16
49
  docxml = svg_unnest(svg_datauri(docxml, docid))
17
50
  isodoc = IsoDoc::PresentationXMLConvert.new({})
18
51
  isodoc.bibitem_lookup(docxml)
19
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)
20
60
  docxml.xpath(ns("//svgmap//#{tag}")).each do |e|
21
- #require "debug"; binding.b
22
61
  svgmap_resolve_eref(e, isodoc, docxml, ids, presxml)
23
62
  end
24
- docxml.xpath(ns("//svgmap/target")).each do |t| # undo Presentation XML: Vectory takes eref not fmt-eref
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|
25
80
  n = t.at(ns(".//fmt-link | .//fmt-xref | .//fmt-eref")) or next
26
81
  n.name = n.name.sub(/^fmt-/, "")
27
82
  t.children = n
28
83
  end
29
- Vectory::SvgMapping.new(docxml, "").call
30
- docxml.xpath(ns("//svgmap")).each { |s| isodoc.svgmap_extract(s) }
31
84
  end
32
85
 
86
+ # Removes nested image elements within svgmap, flattening the structure
33
87
  def svg_unnest(docxml)
34
88
  docxml.xpath(ns("//svgmap//image[.//*[name() = 'image']]")).each do |i|
35
89
  s = i.elements.detect { |e| e.name == "svg" } and
@@ -38,6 +92,7 @@ module Metanorma
38
92
  docxml
39
93
  end
40
94
 
95
+ # Resolves a single EREF element to its target link
41
96
  def svgmap_resolve_eref(eref, isodoc, _docxml, ids, presxml)
42
97
  href = isodoc.eref_target(eref) or return
43
98
  href = href[:link]
@@ -1,3 +1,5 @@
1
+ require "marcel"
2
+
1
3
  module Metanorma
2
4
  class Collection
3
5
  class Renderer
@@ -5,21 +7,25 @@ module Metanorma
5
7
  path = Pathname.new(name)
6
8
  clean_regex = /[<>:"|?*\p{Zs}]/
7
9
  fallback_sym = "_"
8
- return name.gsub(clean_regex, fallback_sym) unless path.absolute?
9
-
10
+ path.absolute? or return name.gsub(clean_regex, fallback_sym)
10
11
  File.join(path.dirname,
11
12
  path.basename.to_s.gsub(clean_regex, fallback_sym))
12
13
  end
13
14
 
14
15
  def dup_bibitem(docid, bib)
15
- newbib = @files.get(docid, :bibdata).dup
16
+ newbib = deep_detached_clone(@files.get(docid, :bibdata))
16
17
  newbib.name = "bibitem"
17
18
  newbib["hidden"] = "true"
18
19
  newbib&.at("./*[local-name() = 'ext']")&.remove
19
20
  newbib["id"] = bib["id"]
21
+ bib["anchor"] and newbib["anchor"] = bib["anchor"]
20
22
  newbib
21
23
  end
22
24
 
25
+ def deep_detached_clone(node)
26
+ Nokogiri::XML(node.to_xml).root
27
+ end
28
+
23
29
  def get_bibitem_docid(bib, identifier)
24
30
  docid =
25
31
  bib.at(ns("./docidentifier[@type = 'metanorma-collection']")) ||
@@ -33,17 +39,10 @@ module Metanorma
33
39
  end
34
40
  end
35
41
 
36
- def hide_refs(docxml)
37
- docxml.xpath(ns("//references[bibitem][not(./bibitem[not(@hidden) or " \
38
- "@hidden = 'false'])]")).each do |f|
39
- f["hidden"] = "true"
40
- end
41
- end
42
-
43
- def new_hidden_ref(xmldoc)
44
- ins = xmldoc.at(ns("bibliography")) or
45
- xmldoc.root << "<bibliography/>" and ins = xmldoc.at(ns("bibliography"))
46
- ins.at(ns("./referenced[@hidden = 'true']")) or
42
+ def new_hidden_ref(xml)
43
+ ins = xml.at(ns("bibliography")) or
44
+ xml.root << "<bibliography/>" and ins = xml.at(ns("bibliography"))
45
+ ins.at(ns("./references[@hidden = 'true']")) or
47
46
  ins.add_child("<references hidden='true' normative='false'/>").first
48
47
  end
49
48
 
@@ -72,16 +71,35 @@ module Metanorma
72
71
  "#{val}</docidentifier>"
73
72
  end
74
73
 
75
- def add_hidden_bibliography(xmldoc, refs)
76
- ins = new_hidden_ref(xmldoc)
74
+ def add_hidden_bibliography(xml, refs, current_id = nil)
75
+ ins = new_hidden_ref(xml)
76
+ current_html = if current_id
77
+ @files.get(current_id,
78
+ :outputs)&.dig(:html)
79
+ else
80
+ nil
81
+ end
77
82
  refs.each do |k, v|
78
83
  url = @files.url(v, {})
84
+ url = make_relative_path(current_html, url) if current_html
79
85
  ins << <<~XML
80
- <bibitem id="#{k}">#{docid_xml(v)}<uri type='citation'>#{url}</uri></bibitem>
86
+ <bibitem id="#{k}" anchor="#{k}">#{docid_xml(v)}<uri type='citation'>#{url}</uri></bibitem>
81
87
  XML
82
88
  end
83
89
  end
84
90
 
91
+ def make_relative_path(from_file, to_file)
92
+ return to_file if to_file.nil? || to_file.to_s.empty?
93
+ return to_file if to_file.start_with?("http://", "https://", "#")
94
+
95
+ from_dir = File.dirname(from_file)
96
+ return to_file if from_dir == "."
97
+
98
+ from_path = Pathname.new(from_dir)
99
+ to_path = Pathname.new(to_file)
100
+ to_path.relative_path_from(from_path).to_s
101
+ end
102
+
85
103
  private
86
104
 
87
105
  def docid_prefix(docid)
@@ -108,63 +126,79 @@ module Metanorma
108
126
  # @raise [ArgumentError]
109
127
  def check_options(options)
110
128
  (options[:format].is_a?(Array) && (FORMATS & options[:format]).any?) or
111
- raise ArgumentError, "Need to specify formats (xml,html,pdf,doc)"
129
+ raise ArgumentError,
130
+ "Need to specify formats (xml,html,pdf,pdf-portfolio,doc)"
112
131
  end
113
132
 
114
- def pdfconv
115
- flavor = @flavor.to_sym
133
+ def pdfconv(added_options)
134
+ flavor, taste_opts, x = pdfconv_prep
135
+ fonts = [taste_opts[:fonts], added_options[:fonts]].compact.flatten
136
+ .join(";")
137
+ opts = @compile_options.merge(added_options.merge(taste_opts))
138
+ opts[:fonts] = fonts
139
+ x.converter.pdf_converter(PdfOptionsNode.new(flavor, opts))
140
+ end
141
+
142
+ def pdfconv_prep
143
+ flavor = Util::taste2flavor(@flavor).to_sym
144
+ taste_opts = Util::taste2isodoc_attrs(@flavor, :pdf)
116
145
  x = Asciidoctor.load nil, backend: flavor
117
- x.converter.pdf_converter(PdfOptionsNode.new(flavor,
118
- @compile_options))
146
+ [flavor, taste_opts, x]
119
147
  end
120
148
 
121
149
  def fail_update_bibitem(docid, identifier)
122
150
  error = "[metanorma] Cannot find crossreference to document #{docid} " \
123
151
  "in document #{identifier}."
124
- @log&.add("Cross-References", nil, error)
152
+ @log&.add("METANORMA_2", nil, params: [docid, identifier])
125
153
  ::Metanorma::Util.log(error, :warning)
126
154
  end
127
155
 
128
156
  def datauri_encode(docxml, directory)
129
157
  docxml.xpath(ns("//image")).each do |i|
130
- read_in_if_svg(i, directory.sub(%r{(?<!=/)$}, "/")) and i["src"] = nil
158
+ read_in_if_svg(i, directory.sub(%r{(?<!=/)$}, "/"))
131
159
  end
132
160
  docxml.xpath(ns("//image")).each do |i| # rubocop:disable Style/CombinableLoops
133
- i["src"] && !i["src"].empty? or next
161
+ (i["src"] && !i["src"].empty?) or next
134
162
  i["src"] = Vectory::Utils::datauri(i["src"], directory)
135
163
  end
136
164
  docxml
137
165
  end
138
166
 
139
167
  def read_in_if_svg(img, localdir)
140
- img["src"] or return false
141
- img.elements.map(&:name).include?("svg") and return true
168
+ img["src"] or return
169
+ if img.elements.map(&:name).include?("svg")
170
+ img["src"] = nil
171
+ return
172
+ end
142
173
  path = Vectory::Utils.svgmap_rewrite0_path(img["src"], localdir)
143
- svg = svg_in_path?(path) or return false
174
+ svg = svg_in_path?(path) or return
144
175
  img.children = (Nokogiri::XML(svg).root)
176
+ img["src"] = nil
145
177
  true
146
178
  end
147
179
 
148
180
  def svg_in_path?(path)
149
181
  File.file?(path) or return false
150
- types = MIME::Types.type_for(path) or return false
151
- types.first == "image/svg+xml" or return false
182
+ type = Marcel::MimeType.for(name: path) or return false
183
+ type == "image/svg+xml" or return false
152
184
  svg = File.read(path, encoding: "utf-8") or return false
153
185
  svg
154
186
  end
155
187
 
156
188
  class PdfOptionsNode
157
189
  def initialize(flavor, options)
158
- p = Metanorma::Registry.instance.find_processor(flavor)
190
+ p = Metanorma::Registry.instance.find_processor(Util::taste2flavor(flavor))
159
191
  if ::Metanorma::Util::FontistHelper.has_custom_fonts?(p, options, {})
160
192
  @fonts_manifest =
161
193
  ::Metanorma::Util::FontistHelper.location_manifest(p, options)
162
194
  end
195
+ @options = options
163
196
  end
164
197
 
165
198
  def attr(key)
166
199
  if key == "fonts-manifest" && @fonts_manifest
167
200
  @fonts_manifest
201
+ else @options[key.to_sym]
168
202
  end
169
203
  end
170
204
  end
@@ -175,11 +209,13 @@ module Metanorma
175
209
  def isodoc_populate
176
210
  @isodoc.info(@xml, nil)
177
211
  { navigation: indexfile(@manifest), nav_object: index_object(@manifest),
178
- bibdata: @bibdata.to_hash, docrefs: liquid_docrefs(@manifest),
179
- "prefatory-content": isodoc_builder(@xml.at("//prefatory-content")),
180
- "final-content": isodoc_builder(@xml.at("//final-content")),
181
- doctitle: @bibdata.title.first.title.content,
182
- docnumber: @bibdata.docidentifier.first.id }.each do |k, v|
212
+ bibdata: YAML.safe_load(@bibdata.to_yaml,
213
+ permitted_classes: [Date, Symbol]),
214
+ docrefs: liquid_docrefs(@manifest),
215
+ "prefatory-content": isodoc_builder(@xml.at("//xmlns:prefatory-content")),
216
+ "final-content": isodoc_builder(@xml.at("//xmlns:final-content")),
217
+ doctitle: Array(@bibdata.title).first&.content,
218
+ docnumber: Array(@bibdata.docidentifier).first&.content }.each do |k, v|
183
219
  v and @isodoc.meta.set(k, v)
184
220
  end
185
221
  end
@@ -187,16 +223,14 @@ module Metanorma
187
223
  def isodoc_builder(node)
188
224
  node or return
189
225
 
190
- # Kludging namespace back in because of Shale brain damage
191
- doc = Nokogiri::XML(node.to_xml.sub(">", " xmlns='http://www.metanorma.org'>"))
192
226
  Nokogiri::HTML::Builder.new(encoding: "UTF-8") do |b|
193
227
  b.div do |div|
194
- doc.root.children&.each { |n| @isodoc.parse(n, div) }
228
+ node.children.each { |n| @isodoc.parse(n, div) }
195
229
  end
196
230
  end.doc.root.to_html
197
231
  end
198
232
 
199
- def eref2link(docxml, presxml)
233
+ def eref2link(docxml, _presxml)
200
234
  isodoc = IsoDoc::PresentationXMLConvert.new({})
201
235
  isodoc.bibitem_lookup(docxml)
202
236
  isodoc.eref2link(docxml)
@@ -205,10 +239,10 @@ module Metanorma
205
239
  def error_anchor(erefs, docid)
206
240
  erefs.each do |e|
207
241
  msg = "<strong>** Unresolved reference to document #{docid} " \
208
- "from eref</strong>"
242
+ "from eref</strong>"
209
243
  e << msg
210
244
  strip_eref(e)
211
- @log&.add("Cross-References", e, msg)
245
+ @log&.add("METANORMA_3", e, params: [docid])
212
246
  end
213
247
  end
214
248