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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40466d3eca9782461819ef5828d00dcdc4203e000cc0a6762e658ac75fab81c2
4
- data.tar.gz: ff68a9e27dd193c8e1263bcb1200f76d87eaf10363ef4c4eb32b9719e9b82eee
3
+ metadata.gz: 66371f7ab35ebb13e3b57cd0dbbaa3ef8a9c0c5d57e99efe4bd1c3452d4055f0
4
+ data.tar.gz: 6bade47b39a3b31a3f74cea3fb119196dd4bf6d87356d39af3f17a5c7dd0fb8e
5
5
  SHA512:
6
- metadata.gz: 77388c12e9b2067cd0e319cdacc3ea914984e5185ce130ea5b6dbc893ab4d1d851294b7f3833811235b3952b50c98ad4830614c87a472dac9a26dc644b48f031
7
- data.tar.gz: 749c67ae8b9c967a0a2446847fb04fc9b045d5f42d4046a66aa79ce0abd5dc373545af2a1520559fa3fb0db4065a51aa2f043fcbe3007b84990ab89b99a286b9
6
+ metadata.gz: 88a35f0eb4334353cb0ae728b82b489ed75514d037b05c66a404d15e230090d10b64dc149487dc65a6f5abb8531807e9b707ec4015de773bf63685f6ae0dc18a
7
+ data.tar.gz: 9ef18c46695f5789fb89e697aeeebb14ef271f411dd11ddecb4a898b3138075fb354f0ece2f1b885987b7d9a852e4627f78f9887c23ee0a605a67325204f9c65
data/.envrc ADDED
@@ -0,0 +1 @@
1
+ use flake
data/.gitignore CHANGED
@@ -22,3 +22,6 @@ relaton/
22
22
  .vscode/
23
23
 
24
24
  .rubocop-https--*
25
+
26
+ # direnv
27
+ .direnv/
data/.rubocop.yml CHANGED
@@ -1,10 +1,31 @@
1
1
  # Auto-generated by Cimas: Do not edit it manually!
2
2
  # See https://github.com/metanorma/cimas
3
3
  inherit_from:
4
- - https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
4
+ - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
5
+ # .rubocop_todo.yml MUST be the last entry. inherit_from is last-wins:
6
+ # if listed before oss-guides, the shared config's stricter Metrics/
7
+ # (MethodLength, BlockLength, etc.) rules override the todo's per-file
8
+ # grandfathering, and the todo becomes inert on those cops. Empirically
9
+ # verified on suma 2026-07-06: with todo listed first, 34 Metrics/* offenses
10
+ # remained; moved last, cleared. Every gem in the metanorma-org fleet
11
+ # already ships a `.rubocop_todo.yml` on its live tree (audited 2026-07-06,
12
+ # 54/54 have it), so this reference is safe to emit unconditionally.
13
+ - .rubocop_todo.yml
14
+
15
+ # Rubocop plugins enabled centrally so every metanorma-org gem picks them up
16
+ # on cimas sync — best practice belongs at the shared-template layer, not
17
+ # per-repo. Per ronaldtse feedback on metanorma/ci#332.
18
+ plugins:
19
+ - rubocop-rspec
20
+ - rubocop-performance
21
+ - rubocop-rake
5
22
 
6
23
  # local repo-specific modifications
7
24
  # ...
8
25
 
9
26
  AllCops:
10
- TargetRubyVersion: 2.5
27
+ # 3.3 matches the org-wide minimum being pushed via #274 (Raise minimum
28
+ # Ruby version to 3.3 due to EOL of 3.2 on 2026-03-31). Was 3.4 before
29
+ # this commit — 3.4 was above the org's stated minimum and would have
30
+ # applied Rubocop rules that fail-close on gems still targeting 3.3.
31
+ TargetRubyVersion: 3.3
data/Gemfile CHANGED
@@ -6,8 +6,25 @@ git_source(:github) { |repo| "https://github.com/#{repo}" }
6
6
 
7
7
  gemspec
8
8
 
9
+ # TEMPORARY cross-PR pin (metanorma-core#18): Compile resolves via Core::Flavors
10
+ gem "metanorma-core", github: "metanorma/metanorma-core", branch: "feat/flavor-table"
11
+
9
12
  begin
10
13
  eval_gemfile("Gemfile.devel")
11
14
  rescue StandardError
12
15
  nil
13
16
  end
17
+
18
+ gem "debug"
19
+ gem "equivalent-xml"
20
+ #gem "metanorma-iho"
21
+ #gem "metanorma-iso", ">= 3.2.0"
22
+ gem "mnconvert"
23
+ gem "pry"
24
+ gem "rake", "~> 13.0"
25
+ gem "rspec", "~> 3.0"
26
+ gem "rspec-command", "~> 1.0"
27
+ gem "rubocop", "~> 1"
28
+ gem "rubocop-performance"
29
+ gem "sassc-embedded", "~> 1"
30
+ gem "simplecov", "~> 0.15"
data/Gemfile.devel ADDED
@@ -0,0 +1,10 @@
1
+ # Local development pins (eval'd by Gemfile; not committed workflow state)
2
+ gem "metanorma-document", github: "metanorma/metanorma-document", branch: "feat/model-validation-l1-declarations"
3
+ gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "feat/move-standard-document"
4
+ gem "metanorma-iso", github: "metanorma/metanorma-iso", branch: "feat/model-validation-migration"
5
+ gem "isodoc", github: "metanorma/isodoc", branch: "main"
6
+ gem "pubid", ">= 2.0.0.pre.alpha.9"
7
+ gem "relaton-cli", ">= 3.0.0.pre.alpha.1"
8
+ gem "glossarist", "~> 2.13"
9
+ gem "lutaml-model", "~> 0.8.0", "< 0.8.20"
10
+ gem "metanorma-plugin-glossarist", "0.3.9"
data/README.adoc CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  image:https://img.shields.io/gem/v/metanorma.svg["Gem Version", link="https://rubygems.org/gems/metanorma"]
4
4
  image:https://github.com/metanorma/metanorma/workflows/rake/badge.svg["Build Status", link="https://github.com/metanorma/metanorma/actions?workflow=rake"]
5
- image:https://codeclimate.com/github/metanorma/metanorma/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/metanorma/metanorma"]
5
+ // image:https://codeclimate.com/github/metanorma/metanorma/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/metanorma/metanorma"]
6
6
  image:https://img.shields.io/github/issues-pr-raw/metanorma/metanorma.svg["Pull Requests", link="https://github.com/metanorma/metanorma/pulls"]
7
7
  image:https://img.shields.io/github/commits-since/metanorma/metanorma/latest.svg["Commits since latest",link="https://github.com/metanorma/metanorma/releases"]
8
8
 
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+
6
+ module Metanorma
7
+ class Collection
8
+ # Durable, content-addressed store for staged collection-build artefacts.
9
+ #
10
+ # Enables incremental (batched) builds and resumption across runs: an
11
+ # artefact is named +<docid-slug>.<content-hash>.<stage>.<format>+, so a
12
+ # document already built with identical inputs is detected by filename alone
13
+ # and reused, and an interrupted run resumes by recomputing which artefacts
14
+ # are already present.
15
+ #
16
+ # Reuse is keyed on the caller-supplied content hash of the document's
17
+ # *input closure* (source + resolved includes + templates + referenced
18
+ # schemas + tool versions + options) -- never on file presence alone. See
19
+ # metanorma/iso-10303#455 for why presence-keyed reuse of compiled XML is
20
+ # unsound; the store deliberately requires a content hash it does not infer.
21
+ #
22
+ # Disabled by default: the collection renderer uses NullArtifactStore unless
23
+ # an incremental build is explicitly requested (opt-in, never default).
24
+ class ArtifactStore
25
+ DEFAULT_DIRNAME = ".metanorma-collection-cache"
26
+
27
+ # The artefact-stage vocabulary, and the one non-redundant property each
28
+ # stage carries: the format it is serialised as (also its file extension).
29
+ # THIS IS ITS HOME: every producer and consumer names its stage from this
30
+ # table by symbol, never a bare string literal elsewhere; an unknown stage
31
+ # raises (KeyError via +fetch+), so the vocabulary cannot drift outside
32
+ # this file. The stage's *name* is the filename token, so it is not stored
33
+ # here -- that would just restate the key. The stages trace the pipeline
34
+ # (isolated compile -> index -> reinflate); each role is documented inline:
35
+ STAGE_FORMATS = {
36
+ # Compiled Semantic XML, unresolved cross-document repo:() references
37
+ # PRESERVED as deterministic stubs (neither stripped nor resolved). The
38
+ # unit of an isolated per-document compile; a pure function of the
39
+ # document's input closure, hence content-addressable and reusable.
40
+ semantic: "xml",
41
+ # This document's contribution to the global anchor index: its anchors
42
+ # and ids (type => {label/UUID => anchor-id}). Aggregated across all
43
+ # documents to build the anchor -> owning-document index used at
44
+ # reinflation.
45
+ anchors: "json",
46
+ # Reinflated Presentation XML: the preserved stubs resolved against the
47
+ # global index into real cross-document links. Shared source for the PDF
48
+ # and Word collection outputs, and per-document for HTML.
49
+ presentation: "xml",
50
+ }.freeze
51
+
52
+ attr_reader :dir
53
+
54
+ def initialize(dir)
55
+ @dir = dir
56
+ FileUtils.mkdir_p(@dir)
57
+ end
58
+
59
+ # <docid-slug>.<content-hash>.<stage>.<format>
60
+ # +stage+ must be a key of STAGE_FORMATS; an unknown stage raises KeyError.
61
+ def path(docid, content_hash, stage)
62
+ format = STAGE_FORMATS.fetch(stage)
63
+ File.join(@dir, "#{slug(docid)}.#{content_hash}.#{stage}.#{format}")
64
+ end
65
+
66
+ # Is this exact (document, input-closure, stage) artefact already present?
67
+ # This is the resumption / skip predicate: a hit is byte-identical to a
68
+ # fresh build because the hash covers every input that determines output.
69
+ def key?(docid, content_hash, stage)
70
+ File.exist?(path(docid, content_hash, stage))
71
+ end
72
+
73
+ def read(docid, content_hash, stage)
74
+ f = path(docid, content_hash, stage)
75
+ File.exist?(f) ? File.read(f, encoding: "UTF-8") : nil
76
+ end
77
+
78
+ # Idempotent write: the same (docid, content_hash, stage) always maps to
79
+ # the same path and the content is a pure function of the hash, so a re-run
80
+ # overwrites with identical bytes -- no side effects on repeat.
81
+ def write(docid, content_hash, stage, content)
82
+ f = path(docid, content_hash, stage)
83
+ File.write(f, content, encoding: "UTF-8")
84
+ f
85
+ end
86
+
87
+ # Remove every stored version of this document whose content hash is not
88
+ # +keep_hash+ -- the superseded artefacts left behind when the document's
89
+ # content changed. Keeps exactly one (current) version per document per
90
+ # stage, bounding disk to current state without losing the cache the next
91
+ # build resumes from.
92
+ def prune_superseded(docid, keep_hash)
93
+ STAGE_FORMATS.each do |stage, ext|
94
+ keep = path(docid, keep_hash, stage)
95
+ Dir[File.join(@dir, "#{slug(docid)}.*.#{stage}.#{ext}")].each do |f|
96
+ f == keep or File.delete(f)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Wipe the entire store. The escape hatch when inputs the content hash
102
+ # cannot see -- imported EXPRESS schemas, templates, included files -- have
103
+ # changed, so the cache can no longer be trusted; the next build recompiles
104
+ # everything. Tracking such shared-input changes is the collection
105
+ # maintainer's responsibility, not the build's.
106
+ def clear
107
+ FileUtils.rm_rf(@dir)
108
+ FileUtils.mkdir_p(@dir)
109
+ end
110
+
111
+ # SHA256 over the input closure, truncated to 16 hex (64 bits -- ample for
112
+ # a per-collection store, and short enough to stay well inside filename
113
+ # length limits). Each input is hashed to a fixed-width digest and the
114
+ # digests concatenated before the final hash, so no two distinct closures
115
+ # collide on the pre-truncation digest. The caller assembles the closure;
116
+ # the store does not infer it.
117
+ def self.content_hash(*inputs)
118
+ digests = inputs.map { |i| Digest::SHA256.hexdigest(i.to_s) }
119
+ Digest::SHA256.hexdigest(digests.join)[0, 16]
120
+ end
121
+
122
+ private
123
+
124
+ def slug(docid)
125
+ docid.to_s.gsub(/[^A-Za-z0-9._-]+/, "-")
126
+ .gsub(/-{2,}/, "-").gsub(/\A-|-\z/, "")
127
+ end
128
+ end
129
+
130
+ # Null object: the store disabled (the default). Never reports a hit, never
131
+ # writes. Lets the renderer treat "no incremental build" as the no-store
132
+ # path without a conditional at every call site (open/closed).
133
+ class NullArtifactStore
134
+ def path(*_args) = nil
135
+ def key?(*_args) = false
136
+ def read(*_args) = nil
137
+ def write(*_args) = nil
138
+ def prune_superseded(*_args) = nil
139
+ def clear(*_args) = nil
140
+ end
141
+ end
142
+ end
@@ -1,26 +1,28 @@
1
- # frozen_string_literal: true
2
-
3
- require "relaton"
4
- require "relaton/cli"
1
+ require "relaton-cli"
5
2
  require "metanorma-utils"
6
3
  require_relative "util/util"
7
4
  require_relative "util/disambig_files"
8
5
  require_relative "config/config"
9
6
  require_relative "config/manifest"
7
+ require_relative "helpers"
10
8
 
11
9
  module Metanorma
12
10
  class FileNotFoundException < StandardError; end
13
11
 
14
12
  class AdocFileNotFoundException < StandardError; end
15
13
 
14
+ # One or more collection members failed to render (#586)
15
+ class RenderFailureException < StandardError; end
16
+
16
17
  # Metanorma collection of documents
17
18
  class Collection
18
- attr_reader :file
19
+ attr_reader :file, :prefatory, :final
19
20
 
20
21
  # @return [Array<String>] documents-inline to inject the XML into
21
22
  # the collection manifest; documents-external to keeps them outside
22
- attr_accessor :directives, :documents, :bibdatas, :coverpage, :dirname
23
- attr_accessor :disambig, :manifest, :bibdata, :compile, :config
23
+ attr_accessor :directives, :documents, :bibdatas, :coverpage,
24
+ :coverpage_pdf_portfolio, :dirname,
25
+ :disambig, :manifest, :bibdata, :compile, :config
24
26
 
25
27
  # @param file [String] path to source file
26
28
  # @param config [Metanorma::Collection::Config]
@@ -28,11 +30,16 @@ module Metanorma
28
30
  def initialize(**args)
29
31
  @file = args[:file]
30
32
  @dirname = File.expand_path(File.dirname(@file)) # feeds @manifest
31
- @documents = args[:documents] || {} # feeds initialize_directives
33
+ @documents = args[:documents] || {} # feeds initialize_directives, initialize_docs
32
34
  @bibdatas = args[:documents] || {}
33
35
  initialize_vars
34
36
  initialize_config(args[:config])
35
37
  initialize_directives
38
+ initialize_docs
39
+ validate_flavor(flavor)
40
+ end
41
+
42
+ def initialize_docs
36
43
  @documents.merge! @manifest.documents
37
44
  @bibdatas.merge! @manifest.documents
38
45
  @documents.transform_keys { |k| Util::key(k) }
@@ -42,23 +49,29 @@ module Metanorma
42
49
  def initialize_vars
43
50
  @compile = Metanorma::Compile.new # feeds @manifest
44
51
  @log = Metanorma::Utils::Log.new
52
+ @log.suppress_log = { severity: 4, category: [], error_ids: [],
53
+ locations: [] }
45
54
  @disambig = Util::DisambigFiles.new
46
55
  end
47
56
 
48
57
  def initialize_config(config)
49
58
  @config = config
50
- @directives = config.directive || [] # feeds initialize_directives
59
+ @directives = (config.directive || []).dup # feeds initialize_directives
51
60
  @bibdata = config.bibdata
52
61
  @prefatory = config.prefatory_content
53
62
  @final = config.final_content
54
63
  @manifest = ::Metanorma::Collection::Manifest
55
64
  .new(config.manifest, self, @dirname) # feeds initialize_directives
65
+ @format = config.format.map(&:to_sym)
66
+ @format&.empty? and @format = nil
56
67
  end
57
68
 
58
69
  def initialize_directives
59
70
  d = @directives.each_with_object({}) { |x, m| m[x.key] = x.value }
60
71
  @coverpage = d["coverpage"]
72
+ @coverpage_pdf_portfolio = d["coverpage-pdf-portfolio"]
61
73
  @coverpage_style = d["coverpage-style"]
74
+ @flavor = d["flavor"]
62
75
  if (@documents.any? || @manifest) && !d.key?("documents-inline") &&
63
76
  !d.key?("documents-external")
64
77
  @directives << ::Metanorma::Collection::Config::Directive
@@ -66,6 +79,10 @@ module Metanorma
66
79
  end
67
80
  end
68
81
 
82
+ def validate_flavor(flavor)
83
+ ::Metanorma::Compile.new.load_flavor(flavor)
84
+ end
85
+
69
86
  def clean_exit
70
87
  @log.write(File.join(@dirname,
71
88
  "#{File.basename(@file, '.*')}.err.html"))
@@ -75,151 +92,172 @@ module Metanorma
75
92
  def to_xml
76
93
  c = ::Metanorma::Collection::Config::Config
77
94
  .new(directive: @directives, bibdata: @bibdata,
78
- manifest: @manifest.config, documents: @documents,
95
+ manifest: @manifest.config, documents: doc_containers,
79
96
  prefatory_content: @prefatory, final_content: @final)
80
97
  c.collection = self
81
- c.to_xml # .sub("<metanorma-collection", "<metanorma-collection xmlns='http://metanorma.org'")
98
+ c.to_xml
99
+ end
100
+
101
+ # Populate the Config#documents collection with DocContainer wrappers.
102
+ # Only emit them when the `documents-inline` directive is set; otherwise
103
+ # the manifest's <entry fileref=…> already references them externally.
104
+ def doc_containers
105
+ @directives.detect { |d| d.key == "documents-inline" } or return []
106
+ @documents.each_with_index.map do |(_, d), idx|
107
+ ::Metanorma::Collection::Config::DocContainer.new(
108
+ id: format("doc%<index>09d", index: idx),
109
+ content: doccontainer1_inner(d),
110
+ )
111
+ end
112
+ end
113
+
114
+ # Inner XML of a single <doc-container> as a string. The body is either
115
+ # the document's <metanorma> presentation root (with its own xmlns) or
116
+ # an attachment payload.
117
+ def doccontainer1_inner(doc)
118
+ b = Nokogiri::XML::Builder.new do |xml|
119
+ xml.send(:wrapper) do |w|
120
+ if doc.attachment
121
+ doc.bibitem and w << doc.bibitem.root.to_xml
122
+ w.attachment Vectory::Utils::datauri(doc.file)
123
+ else
124
+ doc.to_xml w
125
+ w.parent.children.first["flavor"] = Util::taste2flavor(flavor)
126
+ end
127
+ end
128
+ end
129
+ b.parent.elements.first.children.map(&:to_xml).join
82
130
  end
83
131
 
84
132
  def render(opts)
85
- ::Metanorma::Collection::Renderer.render self, opts.merge(log: @log)
133
+ opts[:format].nil? || opts[:format].empty? and
134
+ opts[:format] = @format || [:html]
135
+ opts[:log] = @log
136
+ opts[:flavor] = @flavor
137
+ output_folder(opts)
138
+ cr = ::Metanorma::Collection::Renderer.render self, opts
86
139
  clean_exit
140
+ render_failures_abort(cr)
141
+ cr
87
142
  end
88
143
 
89
- # @return [String, nil]
90
- attr_reader :prefatory, :final
91
-
92
- # @return [String]
93
- def dummy_header
94
- <<~DUMMY
95
- = X
96
- A
144
+ # A member whose rendering fails no longer lets the collection
145
+ # report success with the document missing from the output (#586).
146
+ # Every member is rendered before aborting, so one run reports all
147
+ # failures; the raise makes callers (CLI, suma) exit non-zero. Runs
148
+ # after clean_exit so the failures are in the written log.
149
+ def render_failures_abort(renderer)
150
+ f = renderer.fatal_errors
151
+ f.empty? and return
152
+ raise RenderFailureException.new(
153
+ "Collection render failed for #{f.size} document(s):\n" +
154
+ f.join("\n"),
155
+ )
156
+ end
97
157
 
98
- DUMMY
158
+ def output_folder(opts)
159
+ opts[:output_folder] ||= config.output_folder
160
+ opts[:output_folder] && !Pathname.new(opts[:output_folder]).absolute? and
161
+ opts[:output_folder] = File.join(@dirname, opts[:output_folder])
162
+ warn opts[:output_folder]
99
163
  end
100
164
 
101
165
  # @param elm [String] 'prefatory' or 'final'
102
166
  # @param builder [Nokogiri::XML::Builder]
103
167
  def content_to_xml(elm, builder)
104
168
  (cnt = send(elm)) or return
105
- @compile.load_flavor(doctype)
106
- out = sections(dummy_header + cnt.strip)
169
+ @compile.load_flavor(Util::taste2flavor(flavor))
170
+ out = prefatory_parse(
171
+ Util::asciidoc_dummy_header(
172
+ docidentifier: dummy_header_docidentifier,
173
+ ) + cnt.strip,
174
+ )
107
175
  builder.send("#{elm}-content") { |b| b << out }
108
176
  end
109
177
 
110
- # @param cnt [String] prefatory/final content
111
- # @return [String] XML
112
- def sections(cnt)
113
- c = Asciidoctor.convert(cnt, backend: doctype.to_sym, header_footer: true)
114
- Nokogiri::XML(c, &:huge).at("//xmlns:sections").children.to_xml
178
+ # Pick a real docidentifier value for the prefatory dummy header so
179
+ # the flavor's metadata_id has something pubid-parseable to put into
180
+ # the bibdata, instead of falling back to its (Liquid-templated)
181
+ # docid_template (issue #558). Prefer the first manifest document's
182
+ # docidentifier (concrete, e.g. "IHO S-97"); fall back to the
183
+ # collection's own (which may carry suffixes like "(all parts)" that
184
+ # pubid won't parse). Both candidates are wrapped because the
185
+ # bibitem field may be a parsed Relaton BibliographicItem in some
186
+ # phases and a Nokogiri::XML::Document in others.
187
+ def dummy_header_docidentifier
188
+ first_doc_docid || @bibdata&.docidentifier&.first&.content
115
189
  end
116
190
 
117
- # @param builder [Nokogiri::XML::Builder]
118
- def doccontainer(builder)
119
- # Array(@directives).include? "documents-inline" or return
120
- @directives.detect { |d| d.key == "documents-inline" } or return
121
- documents.each_with_index do |(_, d), i|
122
- doccontainer1(builder, d, i)
191
+ def first_doc_docid
192
+ bib = documents.values.first&.bibitem or return nil
193
+ if bib.respond_to?(:docidentifier)
194
+ bib.docidentifier&.first&.content
195
+ elsif bib.respond_to?(:at) # Nokogiri (rendering-phase shape)
196
+ # Use local-name() so the match works regardless of whether the
197
+ # rendering-phase document carries a default namespace, without
198
+ # tripping Nokogiri's "undefined namespace prefix" error when
199
+ # it doesn't.
200
+ bib.at("//*[local-name()='docidentifier']")&.text
123
201
  end
124
202
  end
125
203
 
126
- def doccontainer1(builder, doc, idx)
127
- id = format("doc%<index>09d", index: idx)
128
- builder.send(:"doc-container", id: id) do |b|
129
- if doc.attachment
130
- doc.bibitem and b << doc.bibitem.root.to_xml
131
- b.attachment Vectory::Utils::datauri(doc.file)
132
- else
133
- doc.to_xml b
134
- end
135
- end
204
+ # @param cnt [String] prefatory/final content
205
+ # @return [String] XML
206
+ def prefatory_parse(cnt)
207
+ x = prefatory_parse_semantic(cnt)
208
+ _, filepath = Util::nokogiri_to_temp(x, "foo", ".presentation.xml")
209
+ c1 = Util::isodoc_create(Util::taste2flavor(@flavor), @manifest.lang,
210
+ @manifest.script, x,
211
+ presxml: true).convert(filepath, nil, true)
212
+ presxml = Nokogiri::XML(c1)
213
+ prefatory_extract_xml(presxml)
136
214
  end
137
215
 
138
- def doctype
139
- @doctype ||= fetch_doctype || "standoc"
216
+ def prefatory_extract_xml(presxml)
217
+ body = presxml.at("//xmlns:sections")
218
+ body.at("//xmlns:p[@class = 'zzSTDTitle1']")&.remove
219
+ body.text.to_s.strip.empty? and body = presxml.at("//xmlns:preface")
220
+ body.at("//xmlns:clause[@type = 'toc']")&.remove
221
+ body.children.to_xml
140
222
  end
141
223
 
142
- def fetch_doctype
143
- docid = @bibdata.docidentifier.first
144
- docid or return
145
- docid.type&.downcase || docid.id&.sub(/\s.*$/, "")&.downcase
224
+ def prefatory_parse_semantic(cnt)
225
+ c = Asciidoctor.convert(cnt, backend: Util::taste2flavor(flavor).to_sym,
226
+ header_footer: true)
227
+ x = Nokogiri::XML(c)
228
+ x.xpath("//xmlns:clause").each { |n| n["unnumbered"] = true }
229
+ b = x.at("//xmlns:bibdata")
230
+ prefatory_parse_fix_bibdata(b)
231
+ b.children = ::Metanorma::Standoc::Cleanup::MergeBibitems
232
+ .new(b.to_xml, @bibdata.to_xml(bibdata: true)).merge.to_noko.children
233
+ x
146
234
  end
147
235
 
148
- class << self
149
- # @param Block [Proc]
150
- # @note allow user-specific function to run in pre-parse model stage
151
- def set_pre_parse_model(&block)
152
- @pre_parse_model_proc = block
153
- end
154
-
155
- # @param Block [Proc]
156
- # @note allow user-specific function to resolve identifier
157
- def set_identifier_resolver(&block)
158
- @identifier_resolver = block
159
- end
160
-
161
- # @param Block [Proc]
162
- # @note allow user-specific function to resolve fileref
163
- # NOTE: MUST ALWAYS RETURN PATH relative to working directory
164
- # (initial YAML file location). @fileref_resolver.call(ref_folder, fileref)
165
- # fileref is not what is in the YAML, but the resolved path
166
- # relative to the working directory
167
- def set_fileref_resolver(&block)
168
- @fileref_resolver = block
169
- end
170
-
171
- def unset_fileref_resolver
172
- @fileref_resolver = nil
173
- end
174
-
175
- # @param collection_model [Hash{String=>String}]
176
- def pre_parse_model(collection_model)
177
- @pre_parse_model_proc or return
178
- @pre_parse_model_proc.call(collection_model)
179
- end
180
-
181
- # @param identifier [String]
182
- # @return [String]
183
- def resolve_identifier(identifier)
184
- @identifier_resolver or return identifier
185
- @identifier_resolver.call(identifier)
186
- end
187
-
188
- # @param fileref [String]
189
- # @return [String]
190
- def resolve_fileref(ref_folder, fileref)
191
- unless @fileref_resolver
192
- (Pathname.new fileref).absolute? or
193
- fileref = File.join(ref_folder, fileref)
194
- return fileref
195
- end
196
-
197
- @fileref_resolver.call(ref_folder, fileref)
236
+ # Stop standoc ownerless copyright from breaking Relaton parse of bibdata
237
+ def prefatory_parse_fix_bibdata(bibdata)
238
+ if cop = bibdata.at("//xmlns:copyright")
239
+ cop.at("./xmlns:owner") or
240
+ cop.children.first.previous =
241
+ "<owner><organization><name>SDO</name></organization></owner>"
198
242
  end
243
+ bibdata
244
+ end
199
245
 
200
- # @param filepath
201
- # @raise [FileNotFoundException]
202
- def check_file_existence(filepath)
203
- unless File.exist?(filepath)
204
- error_message = "#{filepath} not found!"
205
- ::Metanorma::Util.log("[metanorma] Error: #{error_message}", :error)
206
- raise FileNotFoundException.new error_message.to_s
207
- end
208
- end
246
+ def flavor
247
+ @flavor ||= fetch_flavor || "standoc"
248
+ end
209
249
 
210
- def parse(file)
211
- # need @dirname initialised before collection object initialisation
212
- @dirname = File.expand_path(File.dirname(file))
213
- config = case file
214
- when /\.xml$/
215
- ::Metanorma::Collection::Config::Config.from_xml(File.read(file))
216
- when /.ya?ml$/
217
- y = YAML.safe_load(File.read(file))
218
- pre_parse_model(y)
219
- ::Metanorma::Collection::Config::Config.from_yaml(y.to_yaml)
220
- end
221
- new(file: file, config: config)
222
- end
250
+ # TODO: retrieve flavor based on @bibdata publisher when lookup implemented
251
+ # Will still infer based on docid, but will validate it before proceeding
252
+ def fetch_flavor
253
+ docid = @bibdata&.docidentifier&.first or return
254
+ f = docid.type&.downcase || docid.content&.sub(/\s.*$/,
255
+ "")&.downcase or return
256
+ f = Util::taste2flavor(f)
257
+ require ::Metanorma::Compile.new.stdtype2flavor_gem(f)
258
+ f
259
+ rescue LoadError, NameError
260
+ nil
223
261
  end
224
262
  end
225
263
  end
@@ -1,11 +1,11 @@
1
1
  require "relaton"
2
- require "shale"
2
+ require "lutaml/model"
3
3
 
4
4
  module Metanorma
5
5
  class Collection
6
6
  module Config
7
- class Bibdata < ::Shale::Mapper
8
- model ::RelatonBib::BibliographicItem
7
+ class Bibdata < ::Lutaml::Model::Serializable
8
+ model ::Relaton::Bib::ItemData
9
9
  end
10
10
  end
11
11
  end
@@ -1,12 +1,11 @@
1
- require "shale"
1
+ require "lutaml/model"
2
2
 
3
3
  module Metanorma
4
4
  class Collection
5
5
  module Config
6
- class CompileOptions < ::Shale::Mapper
7
- attribute :no_install_fonts, ::Shale::Type::Boolean,
8
- default: -> { true }
9
- attribute :agree_to_terms, ::Shale::Type::Boolean, default: -> { true }
6
+ class CompileOptions < ::Lutaml::Model::Serializable
7
+ attribute :install_fonts, :boolean, default: -> { false }
8
+ attribute :agree_to_terms, :boolean, default: -> { true }
10
9
  end
11
10
  end
12
11
  end