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
@@ -3,6 +3,9 @@
3
3
  require "isodoc"
4
4
  require "metanorma-utils"
5
5
  require_relative "fileparse"
6
+ require_relative "filelocation"
7
+ require_relative "../artifact_store"
8
+ require "json"
6
9
 
7
10
  module Metanorma
8
11
  class Collection
@@ -10,16 +13,78 @@ module Metanorma
10
13
  # compile and output individual file in collection
11
14
  # warn "metanorma compile -x html #{f.path}"
12
15
  def file_compile(file, filename, identifier)
13
- @files.get(identifier, :sectionsplit) and return
14
- opts = {
15
- format: :asciidoc,
16
- extension_keys: @format,
17
- output_dir: @outdir,
18
- type: @flavor,
19
- }.merge(compile_options_update(identifier))
16
+ sectionsplit_val = @files.get(identifier, :sectionsplit)
17
+ sectionsplit_val and return
18
+ opts = compile_options_base(identifier)
19
+ .merge(compile_options_update(identifier))
20
+
21
+ err_count = @compile.errors.size
20
22
  @compile.compile file, opts
21
23
  @files.set(identifier, :outputs, {})
22
- file_compile_formats(filename, identifier)
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('; ')}"
76
+ end
77
+
78
+ def compile_options_base(identifier)
79
+ e = @files.get(identifier, :extract_opts)
80
+ {
81
+ format: :asciidoc,
82
+ extension_keys: @files.get(identifier, :format),
83
+ fonts: e&.dig(:fonts),
84
+ output_dir: @outdir,
85
+ pdffile: @files.get(identifier, :pdffile),
86
+ type: Util::taste2flavor(@flavor),
87
+ }
23
88
  end
24
89
 
25
90
  def compile_options_update(identifier)
@@ -34,28 +99,14 @@ module Metanorma
34
99
  ret
35
100
  end
36
101
 
37
- def file_compile_formats(filename, identifier)
38
- f = @files.get(identifier, :outputs)
39
- concatenate_presentation?({ format: @format }) and
40
- @format << :presentation
41
- @format.each do |e|
42
- ext = @compile.processor.output_formats[e]
43
- fn = File.basename(filename).sub(/(?<=\.)[^.]+$/, ext.to_s)
44
- (/html$/.match?(ext) && @files.get(identifier, :sectionsplit)) or
45
- f[e] = File.join(@outdir, fn)
46
- end
47
- @files.set(identifier, :outputs, f)
48
- end
49
-
50
- def copy_file_to_dest(identifier)
51
- out = Pathname.new(@files.get(identifier, :out_path)).cleanpath
52
- out.absolute? and
53
- out = out.relative_path_from(File.expand_path(FileUtils.pwd))
54
- dest = File.join(@outdir, @disambig.source2dest_filename(out.to_s))
55
- FileUtils.mkdir_p(File.dirname(dest))
56
- source = @files.get(identifier, :ref)
57
- source != dest and FileUtils.cp_r source, dest, remove_destination: true
58
- end
102
+ # def allowed_extension_keys
103
+ # ret = @format.dup
104
+ # @directives.detect { |d| d.key == "individual-pdf" } or
105
+ # ret.delete(:pdf)
106
+ # @directives.detect { |d| d.key == "individual-doc" } or
107
+ # ret.delete(:doc)
108
+ # ret
109
+ # end
59
110
 
60
111
  # process each file in the collection
61
112
  # files are held in memory, and altered as postprocessing
@@ -70,18 +121,50 @@ module Metanorma
70
121
  if @files.get(ident, :attachment) then copy_file_to_dest(ident)
71
122
  else
72
123
  file, fname = @files.targetfile_id(ident, read: true)
124
+ @preserve_unresolved && member_stored?(ident, file) and next
73
125
  warn "\n\n\n\n\nProcess #{fname}: #{DateTime.now.strftime('%H:%M:%S')}"
74
126
  collection_xml = update_xrefs(file, ident, internal_refs)
75
- collection_filename = File.basename(fname, File.extname(fname))
127
+ @preserve_unresolved and
128
+ store_member_artefacts(ident, file, collection_xml)
129
+ # Strip .xml or .html extension, but NOT section numbers like .0
130
+ fname_base = File.basename(fname)
131
+ collection_filename = fname_base
132
+ /\.(xml|html)$/.match?(fname_base) and
133
+ collection_filename = fname_base.sub(/\.(xml|html)$/, "")
76
134
  collection_xml_path = File.join(Dir.tmpdir,
77
135
  "#{collection_filename}.xml")
78
136
  File.write collection_xml_path, collection_xml, encoding: "UTF-8"
79
137
  file_compile(collection_xml_path, fname, ident)
80
- FileUtils.rm(collection_xml_path)
138
+ FileUtils.rm_f(collection_xml_path)
81
139
  end
82
140
  end
83
141
  end
84
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
+
85
168
  # gather internal bibitem references
86
169
  def gather_internal_refs
87
170
  @files.keys.each_with_object({}) do |i, refs|
@@ -135,7 +218,7 @@ module Metanorma
135
218
  ids.each do |id, key|
136
219
  key and next
137
220
  refs[schema][id] = "Missing:#{schema}:#{id}"
138
- @log&.add("Cross-References", nil, refs[schema][id])
221
+ @log&.add("METANORMA_1", nil, params: [refs[schema][id]])
139
222
  end
140
223
  end
141
224
  refs
@@ -155,23 +238,84 @@ module Metanorma
155
238
  def locate_internal_refs1_prep(file)
156
239
  xml = Nokogiri::XML(file, &:huge)
157
240
  r = xml.root["document_suffix"]
158
- xml.xpath("//*[@id]").each_with_object({}) do |i, x|
159
- /^semantic_/.match?(i.name) and next
241
+ ret = xml.xpath("//*[@id]").each_with_object({}) do |i, x|
160
242
  x[i["id"]] = i
161
243
  r and x[i["id"].sub(/_#{r}$/, "")] = i
162
244
  end
245
+ xml.xpath("//*[@anchor]").each do |i|
246
+ ret[i["anchor"]] = i
247
+ r and ret[i["anchor"].sub(/_#{r}$/, "")] = i
248
+ end
249
+ ret
163
250
  end
164
251
 
165
252
  def update_bibitem(bib, identifier)
166
- docid = get_bibitem_docid(bib, identifier) or return
167
- newbib = dup_bibitem(docid, bib)
168
- url = @files.url(docid, relative: true,
169
- doc: !@files.get(docid, :attachment))
170
- dest = newbib.at("./docidentifier") || newbib.at(ns("./docidentifier"))
253
+ newbib, url, attachment = update_bibitem_prep(bib, identifier)
254
+ newbib or return
255
+ dest = begin
256
+ newbib.at("./docidentifier") || newbib.at(ns("./docidentifier"))
257
+ rescue StandardError
258
+ nil
259
+ end
171
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>"
172
265
  dest.previous = "<uri type='citation'>#{url}</uri>"
173
266
  bib.replace(newbib)
174
267
  end
268
+
269
+ def update_bibitem_prep(bib, identifier)
270
+ docid = get_bibitem_docid(bib, identifier) or return [nil, nil, nil]
271
+ newbib = dup_bibitem(docid, bib)
272
+ attachment = @files.get(docid, :attachment)
273
+ url = @files.url(docid, relative: true, doc: !attachment)
274
+ current_html = referencing_html_location(identifier, attachment)
275
+ url = make_relative_path(current_html, url) if current_html
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"
318
+ end
175
319
  end
176
320
  end
177
321
  end
@@ -4,10 +4,10 @@ module Metanorma
4
4
  # @param elm [Nokogiri::XML::Element]
5
5
  # @return [String]
6
6
  def indexfile_title(entry)
7
- if entry.bibdata &&
8
- x = entry.bibdata.title.detect { |t| t.type == "main" } ||
9
- entry.bibdata.title.first
10
- x.title.content
7
+ titles = entry.bibdata && Array(entry.bibdata.title)
8
+ if titles && !titles.empty? &&
9
+ (x = titles.detect { |t| t.type == "main" } || titles.first)
10
+ x.content
11
11
  else
12
12
  entry.title
13
13
  end
@@ -39,9 +39,23 @@ module Metanorma
39
39
  @c.decode(@isodoc.docid_prefix(nil, ident))
40
40
  end
41
41
 
42
+ # Check if file has a recognized MIME type (other than XML)
43
+ # If so, don't append .html (e.g., .svg, .png, .jpg, etc.)
42
44
  def index_link(docref, ident)
43
45
  if docref.file
44
- @files.get(ident, :out_path).sub(/\.xml$/, ".html")
46
+ out_path = @files.get(ident, :out_path)
47
+ # Ensure the path ends with .html for documents, but not for recognized file types
48
+ if Util::mime_file_recognised?(out_path) && !out_path.end_with?(".xml")
49
+ # File has a recognized extension (like .svg, .png), keep it as is
50
+ out_path
51
+ elsif out_path.end_with?(".xml")
52
+ out_path.sub(/\.xml$/, ".html")
53
+ elsif out_path.end_with?(".html")
54
+ out_path
55
+ else
56
+ "#{out_path}.html"
57
+ end
58
+
45
59
  else "#{docref.id}.html"
46
60
  end
47
61
  end
@@ -55,7 +69,7 @@ module Metanorma
55
69
 
56
70
  def index?(mnf)
57
71
  mnf.index and return true
58
- mnf.entry.detect { |e| index?(e) }
72
+ mnf.entry&.detect { |e| index?(e) }
59
73
  end
60
74
 
61
75
  def indexfile1(mnf)
@@ -1,11 +1,13 @@
1
1
  module Metanorma
2
2
  class Collection
3
3
  class Renderer
4
- def docconv
4
+ def docconv(added_options = {})
5
5
  @tempfile_cache ||= []
6
- flavor = @flavor.to_sym
6
+ flavor = Util::taste2flavor(@flavor).to_sym
7
+ opts = Util::taste2isodoc_attrs(@flavor, :doc)
7
8
  x = Asciidoctor.load nil, backend: flavor
8
- x.converter.doc_converter(DocOptionsNode.new(@directives, @dirname))
9
+ x.converter.doc_converter(DocOptionsNode.new(@directives, @dirname,
10
+ added_options.merge(opts)))
9
11
  end
10
12
 
11
13
  def concat_extract_files(filename)
@@ -112,7 +114,7 @@ module Metanorma
112
114
  end
113
115
 
114
116
  class DocOptionsNode
115
- def initialize(directives, dir)
117
+ def initialize(directives, dir, options)
116
118
  @dir = dir
117
119
  @wordcoverpage =
118
120
  Util::hash_key_detect(directives, "document-word-coverpage",
@@ -120,12 +122,14 @@ module Metanorma
120
122
  @wordintropage =
121
123
  Util::hash_key_detect(directives, "document-word-intropage",
122
124
  @wordintropage)
125
+ @options = options
123
126
  end
124
127
 
125
128
  def attr(key)
126
129
  case key
127
130
  when "wordcoverpage" then Util::rel_path_resolve(@dir, @wordcoverpage)
128
131
  when "wordintropage" then Util::rel_path_resolve(@dir, @wordintropage)
132
+ else @options[key.to_sym]
129
133
  end
130
134
  end
131
135
  end