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
@@ -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,15 +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
- }.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
19
22
  @compile.compile file, opts
20
23
  @files.set(identifier, :outputs, {})
21
- 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
+ }
22
88
  end
23
89
 
24
90
  def compile_options_update(identifier)
@@ -33,26 +99,14 @@ module Metanorma
33
99
  ret
34
100
  end
35
101
 
36
- def file_compile_formats(filename, identifier)
37
- f = @files.get(identifier, :outputs)
38
- @format << :presentation if @format.include?(:pdf)
39
- @format.each do |e|
40
- ext = @compile.processor.output_formats[e]
41
- fn = File.basename(filename).sub(/(?<=\.)[^.]+$/, ext.to_s)
42
- (/html$/.match?(ext) && @files.get(identifier, :sectionsplit)) or
43
- f[e] = File.join(@outdir, fn)
44
- end
45
- @files.set(identifier, :outputs, f)
46
- end
47
-
48
- def copy_file_to_dest(identifier)
49
- out = Pathname.new(@files.get(identifier, :out_path)).cleanpath
50
- out.absolute? and
51
- out = out.relative_path_from(File.expand_path(FileUtils.pwd))
52
- dest = File.join(@outdir, @disambig.source2dest_filename(out.to_s))
53
- FileUtils.mkdir_p(File.dirname(dest))
54
- FileUtils.cp @files.get(identifier, :ref), dest
55
- 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
56
110
 
57
111
  # process each file in the collection
58
112
  # files are held in memory, and altered as postprocessing
@@ -67,22 +121,55 @@ 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)
72
- 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)$/, "")
73
134
  collection_xml_path = File.join(Dir.tmpdir,
74
135
  "#{collection_filename}.xml")
75
136
  File.write collection_xml_path, collection_xml, encoding: "UTF-8"
76
137
  file_compile(collection_xml_path, fname, ident)
77
- FileUtils.rm(collection_xml_path)
138
+ FileUtils.rm_f(collection_xml_path)
78
139
  end
79
140
  end
80
141
  end
81
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
+
82
168
  # gather internal bibitem references
83
169
  def gather_internal_refs
84
170
  @files.keys.each_with_object({}) do |i, refs|
85
171
  @files.get(i, :attachment) and next
172
+ @files.get(i, :sectionsplit) and next
86
173
  file, = @files.targetfile_id(i, read: true)
87
174
  gather_internal_refs1(file, i, refs)
88
175
  end
@@ -131,7 +218,7 @@ module Metanorma
131
218
  ids.each do |id, key|
132
219
  key and next
133
220
  refs[schema][id] = "Missing:#{schema}:#{id}"
134
- @log&.add("Cross-References", nil, refs[schema][id])
221
+ @log&.add("METANORMA_1", nil, params: [refs[schema][id]])
135
222
  end
136
223
  end
137
224
  refs
@@ -151,23 +238,84 @@ module Metanorma
151
238
  def locate_internal_refs1_prep(file)
152
239
  xml = Nokogiri::XML(file, &:huge)
153
240
  r = xml.root["document_suffix"]
154
- xml.xpath("//*[@id]").each_with_object({}) do |i, x|
155
- /^semantic_/.match?(i.name) and next
241
+ ret = xml.xpath("//*[@id]").each_with_object({}) do |i, x|
156
242
  x[i["id"]] = i
157
243
  r and x[i["id"].sub(/_#{r}$/, "")] = i
158
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
159
250
  end
160
251
 
161
252
  def update_bibitem(bib, identifier)
162
- docid = get_bibitem_docid(bib, identifier) or return
163
- newbib = dup_bibitem(docid, bib)
164
- url = @files.url(docid, relative: true,
165
- doc: !@files.get(docid, :attachment))
166
- 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
167
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>"
168
265
  dest.previous = "<uri type='citation'>#{url}</uri>"
169
266
  bib.replace(newbib)
170
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
171
319
  end
172
320
  end
173
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)
@@ -114,7 +128,7 @@ module Metanorma
114
128
  files.each { |f| docrefs(f, u) }
115
129
  end
116
130
  end
117
- r.doc.root&.to_html&.gsub("\n", " ")
131
+ r.doc.root&.to_html&.tr("\n", " ")
118
132
  end
119
133
 
120
134
  def liquid_docrefs(mnfs)
@@ -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
- doctype = @doctype.to_sym
7
- x = Asciidoctor.load nil, backend: doctype
8
- x.converter.doc_converter(DocOptionsNode.new(@directives, @dirname))
6
+ flavor = Util::taste2flavor(@flavor).to_sym
7
+ opts = Util::taste2isodoc_attrs(@flavor, :doc)
8
+ x = Asciidoctor.load nil, backend: flavor
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)
@@ -34,7 +36,8 @@ module Metanorma
34
36
 
35
37
  def wrapping_doc_body(doc)
36
38
  doc.xpath(ns("//annex | //preface | //bibliography")).each(&:remove)
37
- s = doc.at(ns("//sections"))
39
+ s = doc.at(ns("//sections")) ||
40
+ doc.root.elements[-1].after("<sections/>").next
38
41
  repl = <<~BODY
39
42
  <sections><clause id='_collection_placeholder'><p>PLACEHOLDER</p></clause></sections>
40
43
  BODY
@@ -50,8 +53,8 @@ module Metanorma
50
53
  doc
51
54
  end
52
55
 
53
- SECTION_BREAK = '<p class="MsoNormal"><br clear="all" class="section"/></p>'
54
- .freeze
56
+ SECTION_BREAK =
57
+ '<p class="MsoNormal"><br clear="all" class="section"/></p>'.freeze
55
58
  DIV1 = '<div class="WordSection1">&#xa0;</div>'.freeze
56
59
  DIV2 = '<div class="WordSection2">&#xa0;</div>'.freeze
57
60
 
@@ -69,10 +72,10 @@ module Metanorma
69
72
  end
70
73
 
71
74
  def collection_coverpages(conv, docs)
72
- conv.wordintropage and [DIV2, SECTION_BREAK].reverse.each do |s|
75
+ conv.wordintropage and [DIV2, SECTION_BREAK].reverse_each do |s|
73
76
  docs.unshift(Nokogiri::XML(s).root)
74
77
  end
75
- conv.wordcoverpage and [DIV1, SECTION_BREAK].reverse.each do |s|
78
+ conv.wordcoverpage and [DIV1, SECTION_BREAK].reverse_each do |s|
76
79
  docs.unshift(Nokogiri::XML(s).root)
77
80
  end
78
81
  docs
@@ -111,7 +114,7 @@ module Metanorma
111
114
  end
112
115
 
113
116
  class DocOptionsNode
114
- def initialize(directives, dir)
117
+ def initialize(directives, dir, options)
115
118
  @dir = dir
116
119
  @wordcoverpage =
117
120
  Util::hash_key_detect(directives, "document-word-coverpage",
@@ -119,12 +122,14 @@ module Metanorma
119
122
  @wordintropage =
120
123
  Util::hash_key_detect(directives, "document-word-intropage",
121
124
  @wordintropage)
125
+ @options = options
122
126
  end
123
127
 
124
128
  def attr(key)
125
129
  case key
126
130
  when "wordcoverpage" then Util::rel_path_resolve(@dir, @wordcoverpage)
127
131
  when "wordintropage" then Util::rel_path_resolve(@dir, @wordintropage)
132
+ else @options[key.to_sym]
128
133
  end
129
134
  end
130
135
  end