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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +27 -0
- data/CLAUDE.md +17 -5
- data/README.adoc +101 -0
- data/lib/suma/anchor_validator.rb +85 -0
- data/lib/suma/artifact_server.rb +102 -0
- data/lib/suma/cli/artifact_server_cli.rb +22 -0
- data/lib/suma/cli/build.rb +4 -0
- data/lib/suma/cli/core.rb +50 -4
- data/lib/suma/cli/export.rb +1 -1
- data/lib/suma/cli/extract_terms.rb +1 -1
- data/lib/suma/cli/generate_register.rb +1 -1
- data/lib/suma/cli/ingest_translations.rb +36 -0
- data/lib/suma/cli/package_gcr.rb +51 -0
- data/lib/suma/cli/validate.rb +16 -0
- data/lib/suma/cli.rb +4 -1
- data/lib/suma/closure_validator.rb +124 -20
- data/lib/suma/corpus_graph.rb +79 -16
- data/lib/suma/gcr_package.rb +230 -0
- data/lib/suma/link_validation_result.rb +8 -0
- data/lib/suma/link_validator.rb +0 -3
- data/lib/suma/output_canonicalizer.rb +107 -0
- data/lib/suma/processor.rb +94 -20
- data/lib/suma/provenance.rb +92 -12
- data/lib/suma/schema_collection.rb +1 -0
- data/lib/suma/schema_compiler.rb +79 -13
- data/lib/suma/schema_manifest_generator.rb +10 -3
- data/lib/suma/schema_profile.rb +12 -0
- data/lib/suma/schema_template/anchors.rb +154 -0
- data/lib/suma/schema_template/document.rb +2 -93
- data/lib/suma/schema_template/plain.rb +10 -6
- data/lib/suma/schema_template.rb +1 -0
- data/lib/suma/site_build.rb +153 -0
- data/lib/suma/staged_collection_builder.rb +11 -3
- data/lib/suma/translation_ingester.rb +169 -0
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +7 -0
- metadata +12 -1
data/lib/suma/schema_compiler.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
57
|
+
collection_files.flat_map do |c|
|
|
53
58
|
collection_data = load_yaml(c)
|
|
54
|
-
collection_data["manifest"]
|
|
55
|
-
|
|
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)
|
data/lib/suma/schema_profile.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
-
[
|
|
33
|
-
|
|
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.
|
|
41
|
+
{{ schema.formatted_hyperlinked_adoc }}
|
|
38
42
|
--
|
|
39
43
|
{% endfor %}
|
|
40
44
|
----
|
data/lib/suma/schema_template.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
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)
|