suma 0.5.8 → 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/.github/workflows/release-provenance.yml +5 -2
- data/.rubocop_todo.yml +27 -0
- data/CLAUDE.md +17 -5
- data/README.adoc +237 -11
- 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/urn_resolver.rb +51 -1
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +7 -0
- metadata +13 -2
data/lib/suma/provenance.rb
CHANGED
|
@@ -83,6 +83,9 @@ module Suma
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# Gems whose versions determine rendered document bytes.
|
|
87
|
+
RENDER_GEM_PREFIXES = %w[metanorma- isodoc].freeze
|
|
88
|
+
|
|
86
89
|
# @return [Hash] toolchain versions that determine output bytes
|
|
87
90
|
def self.toolchain
|
|
88
91
|
{
|
|
@@ -98,30 +101,63 @@ module Suma
|
|
|
98
101
|
versions
|
|
99
102
|
end
|
|
100
103
|
|
|
104
|
+
# Toolchain identity for cached rendered documents: the provenance
|
|
105
|
+
# toolchain plus every installed Metanorma/Isodoc gem, so a renderer or
|
|
106
|
+
# flavor update invalidates cached renders even when source is unchanged.
|
|
107
|
+
def self.render_toolchain
|
|
108
|
+
@render_toolchain ||= toolchain.merge("render" => render_gems)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.render_gems
|
|
112
|
+
gem_specs.select { |spec| render_gem?(spec.name) }
|
|
113
|
+
.sort_by { |spec| [spec.name, spec.version.to_s] }
|
|
114
|
+
.map { |spec| "#{spec.name}-#{spec.version}" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.render_gem?(name)
|
|
118
|
+
name == "metanorma" || RENDER_GEM_PREFIXES.any? do |prefix|
|
|
119
|
+
name.start_with?(prefix)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.gem_specs
|
|
124
|
+
Gem::Specification.select { |spec| spec.name.match?(/\A(?:metanorma|isodoc)/) }
|
|
125
|
+
end
|
|
126
|
+
|
|
101
127
|
# @param inputs [Array<String>] files or directories
|
|
102
128
|
# @param outputs [Array<String>] files or directories
|
|
129
|
+
# @param exclude [Array<String>] files or directories omitted from the
|
|
130
|
+
# manifest and from the unrecorded-output check
|
|
103
131
|
# @return [Hash] manifest
|
|
104
|
-
def self.record(inputs:, outputs:)
|
|
132
|
+
def self.record(inputs:, outputs:, exclude: [])
|
|
133
|
+
excluded = exclude.map { |path| File.expand_path(path) }
|
|
105
134
|
{
|
|
106
135
|
"toolchain" => toolchain,
|
|
107
|
-
"inputs" => digests(inputs),
|
|
108
|
-
"outputs" => digests(outputs),
|
|
136
|
+
"inputs" => digests(inputs, exclude: excluded),
|
|
137
|
+
"outputs" => digests(outputs, exclude: excluded),
|
|
138
|
+
"output_roots" => output_roots(outputs, exclude: excluded),
|
|
139
|
+
"excluded" => excluded.map { |path| relative(path) }.sort,
|
|
109
140
|
}
|
|
110
141
|
end
|
|
111
142
|
|
|
112
143
|
# Write a provenance manifest as canonical JSON.
|
|
113
|
-
def self.write(path, inputs:, outputs:)
|
|
144
|
+
def self.write(path, inputs:, outputs:, exclude: [])
|
|
114
145
|
FileUtils.mkdir_p(File.dirname(path))
|
|
115
|
-
manifest = record(inputs: inputs, outputs: outputs)
|
|
146
|
+
manifest = record(inputs: inputs, outputs: outputs, exclude: exclude)
|
|
116
147
|
File.write(path, "#{JSON.pretty_generate(manifest)}\n")
|
|
117
148
|
path
|
|
118
149
|
end
|
|
119
150
|
|
|
120
|
-
# Independently recompute every digest in the manifest
|
|
151
|
+
# Independently recompute every digest in the manifest and flag files
|
|
152
|
+
# that appeared under a recorded output root but are not in the manifest.
|
|
121
153
|
#
|
|
122
154
|
# @return [Array<String>] problems (empty = verified)
|
|
123
155
|
def self.verify(path)
|
|
124
156
|
manifest = JSON.parse(File.read(path))
|
|
157
|
+
digest_problems(manifest) + unrecorded_outputs(manifest)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.digest_problems(manifest)
|
|
125
161
|
SECTIONS.flat_map do |section|
|
|
126
162
|
manifest.fetch(section, {}).filter_map do |relative, digest|
|
|
127
163
|
full = File.expand_path(relative)
|
|
@@ -134,18 +170,59 @@ module Suma
|
|
|
134
170
|
end
|
|
135
171
|
end
|
|
136
172
|
|
|
173
|
+
def self.unrecorded_outputs(manifest)
|
|
174
|
+
recorded = manifest.fetch("outputs", {})
|
|
175
|
+
excluded = expanded(manifest["excluded"])
|
|
176
|
+
Array(manifest["output_roots"]).flat_map do |root|
|
|
177
|
+
output_files(root)
|
|
178
|
+
.reject { |file| recorded.key?(file) || excluded?(file, excluded) }
|
|
179
|
+
.map { |file| "outputs: unrecorded #{file}" }
|
|
180
|
+
end.sort
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.output_files(root)
|
|
184
|
+
full_root = File.expand_path(root)
|
|
185
|
+
return [] unless File.directory?(full_root)
|
|
186
|
+
|
|
187
|
+
files([full_root]).map { |file| relative(file) }.uniq
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.expanded(paths)
|
|
191
|
+
Array(paths).map { |path| File.expand_path(path) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.excluded?(relative_path, excluded)
|
|
195
|
+
full = File.expand_path(relative_path)
|
|
196
|
+
excluded.any? do |path|
|
|
197
|
+
full == path || full.start_with?("#{path}#{File::SEPARATOR}")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
137
201
|
# @param paths [Array<String>] files or directories
|
|
202
|
+
# @param exclude [Array<String>] expanded paths to skip
|
|
138
203
|
# @return [Hash{String=>String}] relative path → sha256
|
|
139
|
-
def self.digests(paths)
|
|
140
|
-
files
|
|
141
|
-
|
|
142
|
-
end
|
|
143
|
-
files.select { |file| File.file?(file) }
|
|
204
|
+
def self.digests(paths, exclude: [])
|
|
205
|
+
files(paths)
|
|
206
|
+
.reject { |file| excluded?(file, exclude) }
|
|
144
207
|
.map { |file| relative(file) }
|
|
145
208
|
.uniq.sort
|
|
146
209
|
.to_h { |file| [file, Digest::SHA256.file(file).hexdigest] }
|
|
147
210
|
end
|
|
148
211
|
|
|
212
|
+
def self.files(paths)
|
|
213
|
+
paths.flat_map do |path|
|
|
214
|
+
File.directory?(path) ? Dir.glob(File.join(path, "**/*")) : [path]
|
|
215
|
+
end.select { |file| File.file?(file) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Relative paths of the directory entries among +outputs+.
|
|
219
|
+
def self.output_roots(outputs, exclude: [])
|
|
220
|
+
outputs.select { |path| File.directory?(path) }
|
|
221
|
+
.map { |path| relative(path) }
|
|
222
|
+
.reject { |path| excluded?(path, exclude) }
|
|
223
|
+
.uniq.sort
|
|
224
|
+
end
|
|
225
|
+
|
|
149
226
|
def self.relative(path)
|
|
150
227
|
Pathname.new(File.expand_path(path))
|
|
151
228
|
.relative_path_from(Pathname.getwd)
|
|
@@ -154,6 +231,9 @@ module Suma
|
|
|
154
231
|
path.to_s
|
|
155
232
|
end
|
|
156
233
|
|
|
157
|
-
private_class_method :optional_versions, :sign_with, :executable
|
|
234
|
+
private_class_method :optional_versions, :sign_with, :executable?,
|
|
235
|
+
:render_gems, :render_gem?, :gem_specs,
|
|
236
|
+
:digest_problems, :unrecorded_outputs, :output_files,
|
|
237
|
+
:expanded, :excluded?, :files
|
|
158
238
|
end
|
|
159
239
|
end
|
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
|