suma 0.5.9 → 0.5.10

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.
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "fileutils"
5
+ require "json"
4
6
  require "pathname"
5
7
 
6
8
  module Suma
@@ -12,20 +14,32 @@ module Suma
12
14
  # that the caller injects. This split means templates can be tested as pure
13
15
  # functions and the compiler can be tested with a real adoc fixture, without
14
16
  # either depending on the other.
17
+ #
18
+ # Rendering is skipped when the adoc, the schema config, the schema source,
19
+ # and the rendering toolchain are unchanged since the outputs were written:
20
+ # the render key is stored beside the outputs.
15
21
  class SchemaCompiler
22
+ RENDER_KEY_FILE = ".suma-render-key"
23
+
16
24
  attr_reader :schema, :id, :output_path, :template
17
25
 
18
- def initialize(schema:, output_path:, template:)
26
+ def initialize(schema:, output_path:, template:, no_valid: false)
19
27
  @schema = schema
20
28
  @id = schema.id
21
29
  @output_path = output_path
22
30
  @template = template
31
+ @no_valid = no_valid
23
32
  end
24
33
 
25
34
  def compile
26
35
  save_config
27
- save_adoc
36
+ adoc = template.render(config_relative_path)
37
+ save_adoc(adoc)
38
+ key = render_key(adoc)
39
+ return reuse_render if rendered_current?(key)
40
+
28
41
  invoke_metanorma
42
+ write_render_key(key)
29
43
  self
30
44
  end
31
45
 
@@ -37,6 +51,13 @@ module Suma
37
51
  template.extensions
38
52
  end
39
53
 
54
+ # Options handed to Metanorma::Compile. Exposed for specs.
55
+ def compile_options
56
+ { agree_to_terms: true, install_fonts: false }.tap do |options|
57
+ options[:novalid] = true if @no_valid
58
+ end
59
+ end
60
+
40
61
  private
41
62
 
42
63
  def filename_adoc(ext = "adoc")
@@ -47,15 +68,20 @@ module Suma
47
68
  File.join(output_path, "schema_#{id}.yaml")
48
69
  end
49
70
 
50
- def save_adoc
51
- log_relative "Save EXPRESS adoc", filename_adoc
52
-
53
- FileUtils.mkdir_p(File.dirname(filename_adoc))
71
+ def render_key_path
72
+ File.join(output_path, RENDER_KEY_FILE)
73
+ end
54
74
 
55
- config_relative = Pathname.new(filename_config)
75
+ def config_relative_path
76
+ Pathname.new(filename_config)
56
77
  .relative_path_from(Pathname.new(File.dirname(filename_adoc)))
78
+ end
79
+
80
+ def save_adoc(content)
81
+ log_relative "Save EXPRESS adoc", filename_adoc
57
82
 
58
- File.write(filename_adoc, template.render(config_relative))
83
+ FileUtils.mkdir_p(File.dirname(filename_adoc))
84
+ File.write(filename_adoc, content)
59
85
  end
60
86
 
61
87
  def save_config
@@ -68,16 +94,56 @@ module Suma
68
94
  config.save_to_path(filename_config)
69
95
  end
70
96
 
97
+ # Everything the rendered document depends on. The schema source is an
98
+ # input even though the adoc references it only by path.
99
+ def render_key(adoc)
100
+ Digest::SHA256.hexdigest(
101
+ [adoc, File.read(filename_config), schema_source_digest,
102
+ JSON.generate(Provenance.render_toolchain)].join("\0"),
103
+ )
104
+ end
105
+
106
+ def schema_source_digest
107
+ path = schema.path.to_s
108
+ File.file?(path) ? Digest::SHA256.file(path).hexdigest : "missing:#{path}"
109
+ end
110
+
111
+ def rendered_current?(key)
112
+ File.file?(render_key_path) && File.read(render_key_path) == key &&
113
+ expected_output_paths.all? { |path| File.file?(path) }
114
+ end
115
+
116
+ def expected_output_paths
117
+ extensions.split(",").map { |ext| filename_adoc(ext.strip) }
118
+ end
119
+
120
+ def write_render_key(key)
121
+ File.write(render_key_path, key)
122
+ end
123
+
124
+ def reuse_render
125
+ log_relative "Reusing rendered schema doc (id: #{id})", filename_adoc
126
+ self
127
+ end
128
+
71
129
  def invoke_metanorma
72
130
  log_relative "Compiling schema (id: #{id})", filename_adoc
73
- Metanorma::Compile.new.compile(
74
- filename_adoc,
75
- agree_to_terms: true,
76
- install_fonts: false,
77
- )
131
+ Metanorma::Compile.new.compile(filename_adoc, **compile_options)
132
+ verify_outputs!
78
133
  log_relative "Compiling schema (id: #{id}) ... done!", filename_adoc
79
134
  end
80
135
 
136
+ # Metanorma::Compile#compile can return nil for a handled failure instead
137
+ # of raising. Never cache a render that produced no output.
138
+ def verify_outputs!
139
+ missing = expected_output_paths.reject { |path| File.file?(path) }
140
+ return if missing.empty?
141
+
142
+ names = missing.map { |path| File.basename(path) }.join(", ")
143
+ raise CompilationError,
144
+ "Metanorma produced no #{names} for schema #{id}"
145
+ end
146
+
81
147
  def log_relative(prefix, path)
82
148
  relative = Pathname.new(path).relative_path_from(Dir.pwd)
83
149
  Utils.log "#{prefix}: #{relative}"
@@ -48,11 +48,18 @@ exclude_paths: nil)
48
48
  YAML.safe_load(File.read(file_path, encoding: "UTF-8"), aliases: true)
49
49
  end
50
50
 
51
+ # Collection manifest files referenced by the site manifest. Both
52
+ # manifest shapes are handled: legacy `docref` entries and the
53
+ # nested `documents` entries (metanorma 2.x collection manifests).
54
+ # Only `file` references count — `fileref` entries are attachments
55
+ # (documents, schema exports), not collection manifests.
51
56
  def load_manifest_files(collection_files)
52
- collection_files.map do |c|
57
+ collection_files.flat_map do |c|
53
58
  collection_data = load_yaml(c)
54
- collection_data["manifest"]["docref"].map { |docref| docref["file"] }
55
- end.flatten
59
+ manifest = collection_data["manifest"] || {}
60
+ entries = Array(manifest["docref"]) + Array(manifest["documents"])
61
+ entries.filter_map { |entry| entry["file"] }
62
+ end
56
63
  end
57
64
 
58
65
  def load_project_schemas(manifest_files)
@@ -126,9 +126,21 @@ module Suma
126
126
  category_ids: %w[other],
127
127
  )
128
128
 
129
+ # buildingSMART IFC: one standalone schema per edition (IFC4, IFC4X3,
130
+ # ...), entities prefixed Ifc, no ARM/MIM-style decomposition.
131
+ IFC = new(
132
+ id: "ifc",
133
+ rules: [],
134
+ suffix_labels: {},
135
+ acronyms: %w[bim ifc idf 2d 3d 4d 5d],
136
+ acronym_patterns: [/\Aifc\d+(x\d+)*\z/],
137
+ category_ids: %w[other],
138
+ )
139
+
129
140
  REGISTRY = {
130
141
  "iso10303" => ISO_10303,
131
142
  "generic" => GENERIC,
143
+ "ifc" => IFC,
132
144
  }.freeze
133
145
  end
134
146
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ module SchemaTemplate
5
+ # The Liquid anchor block shared by the schema document templates: one
6
+ # [[id]] for every element an Expressir cross-reference may target, so
7
+ # every xref the hyperlinked formatter emits resolves in the rendered
8
+ # document. Expressir links schema-qualified element ids
9
+ # (`schema.entity`) but bare `entity.attribute` pairs, hence the two
10
+ # bookmark forms.
11
+ module Anchors
12
+ # Xref targets are always qualified by the schema's *declared* id, so
13
+ # anchors are too — independent of the manifest key's casing.
14
+ SCHEMA_REF = "{{ schema.id }}"
15
+
16
+ # The raw anchor block contains Liquid comments and newlines that are
17
+ # illegal on the section header line; strip them before interpolating.
18
+ def rendered_anchors
19
+ anchor_block
20
+ .gsub(%r{//[^\r\n]+}, "")
21
+ .gsub(/[\n\r]+/, "")
22
+ .gsub(/^[\n\r]/, "")
23
+ end
24
+
25
+ private
26
+
27
+ def anchor_block
28
+ <<~HEREDOC
29
+ // _fund_cons.liquid
30
+ [[#{schema_id}_funds]]
31
+
32
+ #{constants_and_types_anchors}
33
+
34
+ #{entity_anchors}
35
+
36
+ #{declaration_anchors}
37
+ HEREDOC
38
+ end
39
+
40
+ def constants_and_types_anchors
41
+ <<~HEREDOC
42
+ // _constants.liquid
43
+ {% if schema.constants.size > 0 %}
44
+ #{bookmark('constants')}
45
+ {% for thing in schema.constants %}
46
+ #{bookmark('{{thing.id}}')}
47
+ {% endfor %}
48
+ {% endif %}
49
+
50
+ // _types.liquid
51
+ {% if schema.types.size > 0 %}
52
+ #{bookmark('types')}
53
+ // _type.liquid
54
+ {% for thing in schema.types %}
55
+ #{bookmark('{{thing.id}}')}
56
+ {% if thing.items.size > 0 %}
57
+ // _type_items.liquid
58
+ #{bookmark('{{thing.id}}.items')}
59
+ {% for item in thing.items %}
60
+ #{bookmark('{{thing.id}}.items.{{item.id}}')}
61
+ {% endfor %}
62
+ {% endif %}
63
+ {% endfor %}
64
+ {% endif %}
65
+ HEREDOC
66
+ end
67
+
68
+ def entity_anchors
69
+ <<~HEREDOC
70
+ // _entities.liquid
71
+ {% if schema.entities.size > 0 %}
72
+ #{bookmark('entities')}
73
+ // _entity.liquid
74
+ {% for thing in schema.entities %}
75
+ #{bookmark('{{thing.id}}')}
76
+ {% for attr in thing.attributes %}
77
+ #{bookmark_local('{{thing.id}}.{{attr.id}}')}
78
+ {% endfor %}
79
+ {% endfor %}
80
+ {% endif %}
81
+ HEREDOC
82
+ end
83
+
84
+ def declaration_anchors
85
+ <<~HEREDOC
86
+ // _subtype_constraints.liquid
87
+ {% if schema.subtype_constraints.size > 0 %}
88
+ #{bookmark('subtype_constraints')}
89
+ // _subtype_constraint.liquid
90
+ {% for thing in schema.subtype_constraints %}
91
+ #{bookmark('{{thing.id}}')}
92
+ {% endfor %}
93
+ {% endif %}
94
+
95
+ // _functions.liquid
96
+ {% if schema.functions.size > 0 %}
97
+ #{bookmark('functions')}
98
+ // _function.liquid
99
+ {% for thing in schema.functions %}
100
+ #{callable_anchors}
101
+ {% endfor %}
102
+ {% endif %}
103
+
104
+ // _procedures.liquid
105
+ {% if schema.procedures.size > 0 %}
106
+ #{bookmark('procedures')}
107
+ // _procedure.liquid
108
+ {% for thing in schema.procedures %}
109
+ #{callable_anchors}
110
+ {% endfor %}
111
+ {% endif %}
112
+
113
+ // _rules.liquid
114
+ {% if schema.rules.size > 0 %}
115
+ #{bookmark('rules')}
116
+ // _rule.liquid
117
+ {% for thing in schema.rules %}
118
+ #{bookmark('{{thing.id}}')}
119
+ {% for var in thing.variables %}
120
+ #{bookmark_local('{{thing.id}}.{{var.id}}')}
121
+ {% endfor %}
122
+ {% endfor %}
123
+ {% endif %}
124
+ HEREDOC
125
+ end
126
+
127
+ # Functions and procedures carry parameters and local variables, and
128
+ # Expressir links references to them as bare `owner.name` pairs.
129
+ def callable_anchors
130
+ <<~HEREDOC
131
+ #{bookmark('{{thing.id}}')}
132
+ {% for param in thing.parameters %}
133
+ #{bookmark_local('{{thing.id}}.{{param.id}}')}
134
+ {% endfor %}
135
+ {% for var in thing.variables %}
136
+ #{bookmark_local('{{thing.id}}.{{var.id}}')}
137
+ {% endfor %}
138
+ HEREDOC
139
+ end
140
+
141
+ def bookmark(anchor)
142
+ "[[#{mangled(SCHEMA_REF)}.#{mangled(anchor)}]]"
143
+ end
144
+
145
+ def bookmark_local(anchor)
146
+ "[[#{mangled(anchor)}]]"
147
+ end
148
+
149
+ def mangled(anchor)
150
+ anchor.gsub("}}", ' | replace: "\", "-", "-}}')
151
+ end
152
+ end
153
+ end
154
+ end
@@ -4,10 +4,11 @@ module Suma
4
4
  module SchemaTemplate
5
5
  # Emits an AsciiDoc body with cross-reference anchors for every
6
6
  # schema element so other documents can deep-link into the compiled
7
- # HTML. Only XML is produced — the anchors only resolve against the
7
+ # output. Only XML is produced — the anchors only resolve against the
8
8
  # XML output, not the HTML rendering.
9
9
  class Document
10
10
  EXTENSIONS = "xml"
11
+ include Anchors
11
12
 
12
13
  attr_reader :schema_id
13
14
 
@@ -44,98 +45,6 @@ module Suma
44
45
 
45
46
  ADOC
46
47
  end
47
-
48
- private
49
-
50
- # The raw anchor block contains Liquid comments and newlines that
51
- # are illegal on the section header line; strip them before
52
- # interpolating.
53
- def rendered_anchors
54
- schema_anchors.gsub(%r{//[^\r\n]+}, "").gsub(/[\n\r]+/, "").gsub(
55
- /^[\n\r]/, ""
56
- )
57
- end
58
-
59
- def schema_anchors
60
- <<~HEREDOC
61
- // _fund_cons.liquid
62
- [[#{schema_id}_funds]]
63
-
64
- // _constants.liquid
65
- {% if schema.constants.size > 0 %}
66
- #{bookmark('constants')}
67
- {% for thing in schema.constants %}
68
- #{bookmark('{{thing.id}}')}
69
- {% endfor %}
70
- {% endif %}
71
-
72
- // _types.liquid
73
- {% if schema.types.size > 0 %}
74
- #{bookmark('types')}
75
- // _type.liquid
76
- {% for thing in schema.types %}
77
- #{bookmark('{{thing.id}}')}
78
- {% if thing.items.size > 0 %}
79
- // _type_items.liquid
80
- #{bookmark('{{thing.id}}.items')}
81
- {% for item in thing.items %}
82
- #{bookmark('{{thing.id}}.items.{{item.id}}')}
83
- {% endfor %}
84
- {% endif %}
85
- {% endfor %}
86
- {% endif %}
87
-
88
- // _entities.liquid
89
- {% if schema.entities.size > 0 %}
90
- #{bookmark('entities')}
91
- {% for thing in schema.entities %}
92
- // _entity.liquid
93
- #{bookmark('{{thing.id}}')}
94
- {% endfor %}
95
- {% endif %}
96
-
97
- // _subtype_constraints.liquid
98
- {% if schema.subtype_constraints.size > 0 %}
99
- #{bookmark('subtype_constraints')}
100
- // _subtype_constraint.liquid
101
- {% for thing in schema.subtype_constraints %}
102
- #{bookmark('{{thing.id}}')}
103
- {% endfor %}
104
- {% endif %}
105
-
106
- // _functions.liquid
107
- {% if schema.functions.size > 0 %}
108
- #{bookmark('functions')}
109
- // _function.liquid
110
- {% for thing in schema.functions %}
111
- #{bookmark('{{thing.id}}')}
112
- {% endfor %}
113
- {% endif %}
114
-
115
- // _procedures.liquid
116
- {% if schema.procedures.size > 0 %}
117
- #{bookmark('procedures')}
118
- // _procedure.liquid
119
- {% for thing in schema.procedures %}
120
- #{bookmark('{{thing.id}}')}
121
- {% endfor %}
122
- {% endif %}
123
-
124
- // _rules.liquid
125
- {% if schema.rules.size > 0 %}
126
- #{bookmark('rules')}
127
- // _rule.liquid
128
- {% for thing in schema.rules %}
129
- #{bookmark('{{thing.id}}')}
130
- {% endfor %}
131
- {% endif %}
132
- HEREDOC
133
- end
134
-
135
- def bookmark(anchor)
136
- mangled = anchor.gsub("}}", ' | replace: "\", "-", "-}}')
137
- "[[#{schema_id}.#{mangled}]]"
138
- end
139
48
  end
140
49
  end
141
50
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  module Suma
4
4
  module SchemaTemplate
5
- # Emits a plain AsciiDoc body for a single EXPRESS schema, producing
6
- # both HTML and XML outputs via Metanorma.
5
+ # Emits an anchored, hyperlinked AsciiDoc body for a single EXPRESS
6
+ # schema, producing both HTML and XML outputs via Metanorma. Every
7
+ # element cross-reference the hyperlinked source emits resolves inside
8
+ # the rendered document through the shared anchor block.
7
9
  class Plain
8
10
  EXTENSIONS = "xml,html"
11
+ include Anchors
9
12
 
10
13
  attr_reader :schema_id
11
14
 
@@ -29,12 +32,13 @@ module Suma
29
32
  ----
30
33
  {% for schema in context.schemas %}
31
34
 
32
- [%unnumbered]
33
- == #{schema_id}
35
+ [[#{schema_id}]]
36
+ [%unnumbered,type=express]
37
+ == #{schema_id} #{rendered_anchors}
34
38
 
35
- [source%unnumbered]
39
+ [source%unnumbered,subs="+macros"]
36
40
  --
37
- {{ schema.formatted }}
41
+ {{ schema.formatted_hyperlinked_adoc }}
38
42
  --
39
43
  {% endfor %}
40
44
  ----
@@ -13,6 +13,7 @@ module Suma
13
13
  #
14
14
  # Composition with the compiler lives in SchemaCompiler.
15
15
  module SchemaTemplate
16
+ autoload :Anchors, "suma/schema_template/anchors"
16
17
  autoload :Plain, "suma/schema_template/plain"
17
18
  autoload :Document, "suma/schema_template/document"
18
19
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+ require "pathname"
7
+
8
+ module Suma
9
+ # Freshness decision for the rendered collection site.
10
+ #
11
+ # The collection render is expensive: Metanorma compiles every member
12
+ # document. Its output is a pure function of the emitted collection manifest,
13
+ # the source tree it references, and the rendering toolchain, so this class
14
+ # keys those inputs, remembers the key and the produced files in the output
15
+ # directory, and lets the processor skip the render when nothing changed.
16
+ class SiteBuild
17
+ STATE_FILE = ".suma-site-build.json"
18
+
19
+ # Emitted manifests that the build itself regenerates deterministically
20
+ # from the sources above; whether they exist yet must not change the key.
21
+ GENERATED_FILES = %w[collection-output.yaml schemas.yml].freeze
22
+
23
+ def initialize(root:, output_directory:, inputs: [])
24
+ @root = File.expand_path(root)
25
+ @output_directory = File.expand_path(output_directory)
26
+ @extra_inputs = inputs.map { |path| File.expand_path(path) }
27
+ end
28
+
29
+ # @return [Array<String>] source files determining the rendered site
30
+ def source_files
31
+ @source_files ||= begin
32
+ files = @extra_inputs.select { |file| File.file?(file) }
33
+ (files + tree_files).uniq.sort
34
+ end
35
+ end
36
+
37
+ # @return [String] digest of every source file and the render toolchain
38
+ def key
39
+ @key ||= begin
40
+ digest = Digest::SHA256.new
41
+ source_files.each do |file|
42
+ digest << relative(file) << "\0"
43
+ digest << Digest::SHA256.file(file).hexdigest << "\0"
44
+ end
45
+ digest << JSON.generate(Provenance.render_toolchain)
46
+ digest.hexdigest
47
+ end
48
+ end
49
+
50
+ # True when the previous render used the same inputs and every file it
51
+ # produced is still present.
52
+ def current?
53
+ state = read_state
54
+ return false unless state && state["key"] == key
55
+
56
+ outputs = Array(state["outputs"])
57
+ !outputs.empty? &&
58
+ outputs.all? { |file| File.file?(File.join(@output_directory, file)) }
59
+ end
60
+
61
+ # Persist the key and the produced files for the next build.
62
+ def record
63
+ write_state("key" => key, "outputs" => output_files)
64
+ self
65
+ end
66
+
67
+ def state_path
68
+ File.join(@output_directory, STATE_FILE)
69
+ end
70
+
71
+ private
72
+
73
+ def tree_files
74
+ excluded = excluded_paths
75
+ Dir.glob(File.join(@root, "**", "*")).select do |file|
76
+ File.file?(file) &&
77
+ excluded.none? { |path| under?(file, path) } &&
78
+ !generated?(relative(file).tr("\\", "/"))
79
+ end
80
+ end
81
+
82
+ # Files the collection render writes next to its sources. They are
83
+ # outputs of the previous build, never inputs, so including them would
84
+ # change the key on every run.
85
+ def generated?(relative)
86
+ base = File.basename(relative)
87
+ GENERATED_FILES.include?(relative) ||
88
+ relative.start_with?("relaton/") ||
89
+ base.start_with?("tmp_document") ||
90
+ base.end_with?("_index.html", ".err.html", ".log.txt") ||
91
+ document_artifact?(relative, base)
92
+ end
93
+
94
+ def document_artifact?(relative, base)
95
+ return false unless relative.start_with?("documents/")
96
+
97
+ base == "cover.html" || base == "document.xml" ||
98
+ (base.start_with?("document.") && base.include?(".xml"))
99
+ end
100
+
101
+ # Generated trees are derived from the sources above and never feed the
102
+ # site render as inputs of their own. Bundler's install directory is a
103
+ # dependency artifact: CI conventionally places it at vendor/bundle
104
+ # inside the project, where it must never count as site source.
105
+ def excluded_paths
106
+ fixed = %w[schema_docs plain_schemas .git .bundle vendor/bundle]
107
+ .map { |dir| File.join(@root, dir) }
108
+ env = [ENV.fetch("SUMA_SCHEMA_CACHE_DIR", nil),
109
+ ENV.fetch("BUNDLE_PATH", nil)]
110
+ .compact
111
+ .map { |dir| File.expand_path(dir, @root) }
112
+ [@output_directory] + fixed + env
113
+ end
114
+
115
+ def under?(file, path)
116
+ file == path || file.start_with?("#{path}#{File::SEPARATOR}")
117
+ end
118
+
119
+ def read_state
120
+ return unless File.file?(state_path)
121
+
122
+ JSON.parse(File.read(state_path))
123
+ rescue JSON::ParserError
124
+ nil
125
+ end
126
+
127
+ def write_state(state)
128
+ FileUtils.mkdir_p(@output_directory)
129
+ temp = "#{state_path}.#{Process.pid}.tmp"
130
+ File.write(temp, "#{JSON.pretty_generate(state)}\n")
131
+ File.rename(temp, state_path)
132
+ ensure
133
+ File.delete(temp) if temp && File.exist?(temp)
134
+ end
135
+
136
+ def output_files
137
+ Dir.glob(File.join(@output_directory, "**", "*"))
138
+ .select do |file|
139
+ File.file?(file) && File.basename(file) != STATE_FILE
140
+ end
141
+ .map { |file| relative_to(file, @output_directory) }
142
+ .sort
143
+ end
144
+
145
+ def relative(path)
146
+ relative_to(path, @root)
147
+ end
148
+
149
+ def relative_to(path, base)
150
+ Pathname.new(path).relative_path_from(Pathname.new(base)).to_s
151
+ end
152
+ end
153
+ end
@@ -37,14 +37,16 @@ module Suma
37
37
  # +collection_config_path+ is the emitted collection manifest (the same
38
38
  # +collection-output.yaml+ the normal build renders). Member filerefs are
39
39
  # relative to its directory, so all per-member manifests are written there.
40
+ # (a data-declaring initializer)
40
41
  def initialize(collection_config_path:, output_directory:, coverpage: nil,
41
- store_dir: nil, formats: %i[xml html])
42
+ store_dir: nil, formats: %i[xml html], no_valid: false) # rubocop:disable Metrics/ParameterLists
42
43
  @config_path = collection_config_path
43
44
  @config_dir = File.dirname(File.expand_path(collection_config_path))
44
45
  @output_directory = output_directory
45
46
  @coverpage = coverpage
46
47
  @store_dir = store_dir || File.join(output_directory, STORE_DIRNAME)
47
48
  @formats = formats
49
+ @no_valid = no_valid
48
50
  end
49
51
 
50
52
  def build
@@ -97,7 +99,7 @@ module Suma
97
99
  manifest_path: one_path,
98
100
  opts: { format: [:xml], output_folder: out,
99
101
  preserve_unresolved: true, artifact_store_dir: @store_dir,
100
- compile: { install_fonts: false } },
102
+ compile: compile_flags },
101
103
  )
102
104
  ensure
103
105
  FileUtils.rm_f(one_path)
@@ -118,10 +120,16 @@ module Suma
118
120
  manifest_path: reinf_path,
119
121
  opts: { format: @formats, output_folder: @output_directory,
120
122
  reinflate: true, coverpage: @coverpage,
121
- compile: { install_fonts: false } }.compact,
123
+ compile: compile_flags }.compact,
122
124
  )
123
125
  end
124
126
 
127
+ def compile_flags
128
+ { install_fonts: false }.tap do |flags|
129
+ flags[:novalid] = true if @no_valid
130
+ end
131
+ end
132
+
125
133
  # Copy each member's stored stub into +dir+ and return docref entries pointing
126
134
  # at the copies, keyed by real docid (as the store keys them).
127
135
  def stored_docrefs(members, dir)