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
|
@@ -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)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "glossarist"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Suma
|
|
8
|
+
# Overlays translated content onto extracted Glossarist concepts (T5).
|
|
9
|
+
#
|
|
10
|
+
# A translations file is a YAML sequence of localized-concept documents,
|
|
11
|
+
# each carrying an identity key plus the content to apply:
|
|
12
|
+
#
|
|
13
|
+
# - concept_id: example_schema.bolt # managed id (data.identifier)
|
|
14
|
+
# # or the managed concept's uuid
|
|
15
|
+
# - id: <localized uuid> # alternative identity key
|
|
16
|
+
# data:
|
|
17
|
+
# language_code: fra
|
|
18
|
+
# terms: [...]
|
|
19
|
+
# definition: [...]
|
|
20
|
+
#
|
|
21
|
+
# Every field the entry provides replaces the localization's field; every
|
|
22
|
+
# field it omits falls back, explicitly, to the localization's current
|
|
23
|
+
# content (for a new language: the source-language localization's). All
|
|
24
|
+
# (de)serialization goes through the Glossarist/lutaml-model framework;
|
|
25
|
+
# entries are merged as serialized hashes and reified with of_yaml.
|
|
26
|
+
class TranslationIngester
|
|
27
|
+
def initialize(concepts_dir:, translations:, output_dir: nil)
|
|
28
|
+
@concepts_dir = concepts_dir
|
|
29
|
+
@translations = translations
|
|
30
|
+
@output_dir = output_dir || concepts_dir
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash{String=>Array<String>}] concept file → applied entry
|
|
34
|
+
# identities
|
|
35
|
+
def ingest
|
|
36
|
+
entries = load_entries
|
|
37
|
+
applied = {}
|
|
38
|
+
each_concept_document do |file, doc|
|
|
39
|
+
matched = entries_for(doc, entries)
|
|
40
|
+
next if matched.empty?
|
|
41
|
+
|
|
42
|
+
applied[file] = matched.map { |entry| apply_entry(doc, entry) }
|
|
43
|
+
write_document(file, doc)
|
|
44
|
+
end
|
|
45
|
+
assert_no_unmatched!(entries, applied)
|
|
46
|
+
applied
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def load_entries
|
|
52
|
+
content = YAML.safe_load_file(@translations, permitted_classes: [],
|
|
53
|
+
aliases: true)
|
|
54
|
+
entries = content.is_a?(Hash) ? content["translations"] : content
|
|
55
|
+
unless entries.is_a?(Array)
|
|
56
|
+
raise Error,
|
|
57
|
+
"translations file must be a YAML sequence of localized " \
|
|
58
|
+
"concepts: #{@translations}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
entries
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def each_concept_document
|
|
65
|
+
Dir.glob(File.join(@concepts_dir, "*.yaml")).each do |file|
|
|
66
|
+
doc = Glossarist::V3::ConceptDocument.from_yamls(File.read(file))
|
|
67
|
+
yield file, doc
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def entries_for(doc, entries)
|
|
72
|
+
entries.select { |entry| entry_matches?(doc, entry) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def entry_matches?(doc, entry)
|
|
76
|
+
if entry["concept_id"]
|
|
77
|
+
[doc.concept.data.id, doc.concept.uuid].include?(entry["concept_id"])
|
|
78
|
+
else
|
|
79
|
+
localized_uuids(doc).include?(entry["id"])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def localized_uuids(doc)
|
|
84
|
+
doc.localizations.map(&:id) +
|
|
85
|
+
doc.concept.data.localized_concepts.values
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_entry(doc, entry)
|
|
89
|
+
incoming = Glossarist::LocalizedConcept
|
|
90
|
+
.of_yaml(entry.except("concept_id"))
|
|
91
|
+
localized = merge_localization(doc, entry, incoming)
|
|
92
|
+
replace_localization(doc, localized)
|
|
93
|
+
entry["concept_id"] || entry["id"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def replace_localization(doc, localized)
|
|
97
|
+
language = localized.data.language_code
|
|
98
|
+
doc.localizations.reject! { |l10n| l10n.data.language_code == language }
|
|
99
|
+
doc.localizations << localized
|
|
100
|
+
doc.concept.data.localized_concepts[language] = localized.uuid
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Entry fields win; absent fields keep the localization's current
|
|
104
|
+
# content (or the source-language content for a new language). The
|
|
105
|
+
# merge happens on the raw entry data — a parsed model would
|
|
106
|
+
# materialize empty collections and wipe the fields it omits.
|
|
107
|
+
#
|
|
108
|
+
# Glossarist parses the serialized "id" key into `#id` (not `#uuid`),
|
|
109
|
+
# so the persisted id is preserved and stamped back onto the merged
|
|
110
|
+
# localization explicitly; a new language gets the same v5 id
|
|
111
|
+
# TermExtractor's linked localizations use.
|
|
112
|
+
def merge_localization(doc, entry, incoming)
|
|
113
|
+
current = current_localization(doc, entry, incoming)
|
|
114
|
+
base = current || doc.localizations.first
|
|
115
|
+
return incoming unless base
|
|
116
|
+
|
|
117
|
+
identity = entry["id"] || current&.id ||
|
|
118
|
+
new_localization_uuid(doc, incoming.data.language_code)
|
|
119
|
+
localized = Glossarist::LocalizedConcept
|
|
120
|
+
.of_yaml(merged_hash(base, entry, identity))
|
|
121
|
+
localized.uuid = identity
|
|
122
|
+
localized
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def merged_hash(base, entry, identity)
|
|
126
|
+
merged = base.to_yaml_hash
|
|
127
|
+
merged["data"] = (merged["data"] || {})
|
|
128
|
+
.merge(entry["data"] || {})
|
|
129
|
+
merged["id"] = identity
|
|
130
|
+
merged
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def current_localization(doc, entry, incoming)
|
|
134
|
+
if entry["id"]
|
|
135
|
+
doc.localizations.find { |l10n| l10n.id == entry["id"] }
|
|
136
|
+
else
|
|
137
|
+
doc.localizations.find do |l10n|
|
|
138
|
+
l10n.data.language_code == incoming.data.language_code
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Same convention as TermExtractor's linked localizations.
|
|
144
|
+
def new_localization_uuid(doc, language)
|
|
145
|
+
Glossarist::Utilities::UUID.uuid_v5(
|
|
146
|
+
Glossarist::Utilities::UUID::OID_NAMESPACE,
|
|
147
|
+
"#{doc.concept.data.id}-#{language}",
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write_document(file, doc)
|
|
152
|
+
output = File.join(@output_dir, File.basename(file))
|
|
153
|
+
FileUtils.mkdir_p(@output_dir) if output != file
|
|
154
|
+
File.write(output, doc.to_yamls, encoding: "utf-8")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def assert_no_unmatched!(entries, applied)
|
|
158
|
+
matched = applied.values.flatten
|
|
159
|
+
unmatched = entries.reject do |entry|
|
|
160
|
+
matched.include?(entry["concept_id"] || entry["id"])
|
|
161
|
+
end
|
|
162
|
+
return if unmatched.empty?
|
|
163
|
+
|
|
164
|
+
raise Error,
|
|
165
|
+
"unmatched translation entries: " \
|
|
166
|
+
"#{unmatched.map { |e| e['concept_id'] || e['id'] }.join(', ')}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/suma/urn_resolver.rb
CHANGED
|
@@ -53,9 +53,12 @@ module Suma
|
|
|
53
53
|
)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Write JSON and HTML for every artifact.
|
|
56
|
+
# Write JSON and HTML for every artifact, plus an index of the set.
|
|
57
57
|
def write(output_directory)
|
|
58
58
|
artifacts.each_key { |key| write_urn(key, output_directory) }
|
|
59
|
+
FileUtils.mkdir_p(output_directory)
|
|
60
|
+
File.write(File.join(output_directory, "index.html"),
|
|
61
|
+
index_document, encoding: "utf-8")
|
|
59
62
|
Utils.log "Wrote #{artifacts.length} URN artifacts to #{output_directory}"
|
|
60
63
|
end
|
|
61
64
|
|
|
@@ -97,11 +100,38 @@ module Suma
|
|
|
97
100
|
payload = element_payload(schema, element)
|
|
98
101
|
artifacts[compose(:tech, "#{schema.id}.#{element.id}")] = payload if
|
|
99
102
|
payload
|
|
103
|
+
add_attribute_artifacts(artifacts, schema, element)
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
artifacts
|
|
103
107
|
end
|
|
104
108
|
|
|
109
|
+
# Attribute property IRIs appear in the OWL projection as
|
|
110
|
+
# <schema>.<entity>.<attribute>; make them dereferenceable too.
|
|
111
|
+
def add_attribute_artifacts(artifacts, schema, element)
|
|
112
|
+
return unless element.is_a?(Expressir::Model::Declarations::Entity)
|
|
113
|
+
|
|
114
|
+
element.attributes.each do |attribute|
|
|
115
|
+
key = compose(:tech,
|
|
116
|
+
"#{schema.id}.#{element.id}.#{attribute.id}")
|
|
117
|
+
artifacts[key] = attribute_artifact(schema, element, attribute)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def attribute_artifact(schema, entity, attribute)
|
|
122
|
+
{
|
|
123
|
+
"urn" => compose(:tech, "#{schema.id}.#{entity.id}.#{attribute.id}"),
|
|
124
|
+
"kind" => "attribute",
|
|
125
|
+
"schema" => schema.id,
|
|
126
|
+
"entity" => entity.id,
|
|
127
|
+
"id" => attribute.id,
|
|
128
|
+
"attribute_kind" => attribute.kind,
|
|
129
|
+
"optional" => attribute.optional == true,
|
|
130
|
+
"type" => attribute.type&.source,
|
|
131
|
+
"source" => relative_source(schema),
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
105
135
|
def schemas
|
|
106
136
|
@schemas ||= begin
|
|
107
137
|
manifest = Expressir::SchemaManifest.from_file(@manifest_file)
|
|
@@ -310,6 +340,26 @@ module Suma
|
|
|
310
340
|
HTML
|
|
311
341
|
end
|
|
312
342
|
|
|
343
|
+
# A deterministic index of every resolved URN, linked to its document.
|
|
344
|
+
def index_document
|
|
345
|
+
items = artifacts.keys.sort.map do |urn|
|
|
346
|
+
href = artifact_path(urn, "html")
|
|
347
|
+
%( <li><a href="#{escape_html(href)}">#{escape_html(urn)}</a></li>)
|
|
348
|
+
end.join("\n")
|
|
349
|
+
<<~HTML
|
|
350
|
+
<!doctype html>
|
|
351
|
+
<html lang="en">
|
|
352
|
+
<head><meta charset="utf-8"><title>Resolved URNs</title></head>
|
|
353
|
+
<body>
|
|
354
|
+
<h1>Resolved URNs</h1>
|
|
355
|
+
<ul>
|
|
356
|
+
#{items}
|
|
357
|
+
</ul>
|
|
358
|
+
</body>
|
|
359
|
+
</html>
|
|
360
|
+
HTML
|
|
361
|
+
end
|
|
362
|
+
|
|
313
363
|
def escape_html(value)
|
|
314
364
|
value.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
315
365
|
.gsub('"', """)
|
data/lib/suma/version.rb
CHANGED
data/lib/suma.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "fileutils"
|
|
|
7
7
|
module Suma
|
|
8
8
|
autoload :VERSION, "suma/version"
|
|
9
9
|
|
|
10
|
+
autoload :AnchorValidator, "suma/anchor_validator"
|
|
11
|
+
autoload :ArtifactServer, "suma/artifact_server"
|
|
10
12
|
autoload :SchemaProfile, "suma/schema_profile"
|
|
11
13
|
autoload :BuildPlan, "suma/build_plan"
|
|
12
14
|
autoload :Processor, "suma/processor"
|
|
@@ -20,14 +22,17 @@ module Suma
|
|
|
20
22
|
autoload :CorpusGraph, "suma/corpus_graph"
|
|
21
23
|
autoload :CollectionManifest, "suma/collection_manifest"
|
|
22
24
|
autoload :EengineConverter, "suma/eengine_converter"
|
|
25
|
+
autoload :GcrPackage, "suma/gcr_package"
|
|
23
26
|
autoload :ExpressReformatter, "suma/express_reformatter"
|
|
24
27
|
autoload :ExpressSchema, "suma/express_schema"
|
|
25
28
|
autoload :LinkValidation, "suma/link_validation"
|
|
29
|
+
autoload :LinkValidationResult, "suma/link_validation_result"
|
|
26
30
|
autoload :LinkValidator, "suma/link_validator"
|
|
27
31
|
autoload :ManifestTraverser, "suma/manifest_traverser"
|
|
28
32
|
autoload :MappingDrift, "suma/mapping_drift"
|
|
29
33
|
autoload :NoteProcessor, "suma/note_processor"
|
|
30
34
|
autoload :NullCache, "suma/null_cache"
|
|
35
|
+
autoload :OutputCanonicalizer, "suma/output_canonicalizer"
|
|
31
36
|
autoload :OwlProjection, "suma/owl_projection"
|
|
32
37
|
autoload :RegisterManifestGenerator, "suma/register_manifest_generator"
|
|
33
38
|
autoload :SchemaCache, "suma/schema_cache"
|
|
@@ -42,11 +47,13 @@ module Suma
|
|
|
42
47
|
autoload :SchemaNaming, "suma/schema_naming"
|
|
43
48
|
autoload :SchemaTemplate, "suma/schema_template"
|
|
44
49
|
autoload :SiteConfig, "suma/site_config"
|
|
50
|
+
autoload :SiteBuild, "suma/site_build"
|
|
45
51
|
autoload :StagedCollectionBuilder, "suma/staged_collection_builder"
|
|
46
52
|
autoload :SvgQuality, "suma/svg_quality"
|
|
47
53
|
autoload :TermClassification, "suma/term_classification"
|
|
48
54
|
autoload :TermExtractor, "suma/term_extractor"
|
|
49
55
|
autoload :ThorExt, "suma/thor_ext"
|
|
56
|
+
autoload :TranslationIngester, "suma/translation_ingester"
|
|
50
57
|
autoload :Urn, "suma/urn"
|
|
51
58
|
autoload :UrnResolver, "suma/urn_resolver"
|
|
52
59
|
autoload :Utils, "suma/utils"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: suma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: expressir
|
|
@@ -185,11 +185,14 @@ files:
|
|
|
185
185
|
- bin/setup
|
|
186
186
|
- exe/suma
|
|
187
187
|
- lib/suma.rb
|
|
188
|
+
- lib/suma/anchor_validator.rb
|
|
189
|
+
- lib/suma/artifact_server.rb
|
|
188
190
|
- lib/suma/build_plan.rb
|
|
189
191
|
- lib/suma/change_classification.rb
|
|
190
192
|
- lib/suma/change_impact.rb
|
|
191
193
|
- lib/suma/change_manifest.rb
|
|
192
194
|
- lib/suma/cli.rb
|
|
195
|
+
- lib/suma/cli/artifact_server_cli.rb
|
|
193
196
|
- lib/suma/cli/build.rb
|
|
194
197
|
- lib/suma/cli/check_svg_quality.rb
|
|
195
198
|
- lib/suma/cli/compare.rb
|
|
@@ -199,6 +202,8 @@ files:
|
|
|
199
202
|
- lib/suma/cli/extract_terms.rb
|
|
200
203
|
- lib/suma/cli/generate_register.rb
|
|
201
204
|
- lib/suma/cli/generate_schemas.rb
|
|
205
|
+
- lib/suma/cli/ingest_translations.rb
|
|
206
|
+
- lib/suma/cli/package_gcr.rb
|
|
202
207
|
- lib/suma/cli/reformat.rb
|
|
203
208
|
- lib/suma/cli/resolve.rb
|
|
204
209
|
- lib/suma/cli/validate.rb
|
|
@@ -213,17 +218,20 @@ files:
|
|
|
213
218
|
- lib/suma/eengine_converter.rb
|
|
214
219
|
- lib/suma/express_reformatter.rb
|
|
215
220
|
- lib/suma/express_schema.rb
|
|
221
|
+
- lib/suma/gcr_package.rb
|
|
216
222
|
- lib/suma/jsdai.rb
|
|
217
223
|
- lib/suma/jsdai/figure.rb
|
|
218
224
|
- lib/suma/jsdai/figure_image.rb
|
|
219
225
|
- lib/suma/jsdai/figure_xml.rb
|
|
220
226
|
- lib/suma/link_validation.rb
|
|
227
|
+
- lib/suma/link_validation_result.rb
|
|
221
228
|
- lib/suma/link_validator.rb
|
|
222
229
|
- lib/suma/link_validator/step.rb
|
|
223
230
|
- lib/suma/manifest_traverser.rb
|
|
224
231
|
- lib/suma/mapping_drift.rb
|
|
225
232
|
- lib/suma/note_processor.rb
|
|
226
233
|
- lib/suma/null_cache.rb
|
|
234
|
+
- lib/suma/output_canonicalizer.rb
|
|
227
235
|
- lib/suma/owl_projection.rb
|
|
228
236
|
- lib/suma/processor.rb
|
|
229
237
|
- lib/suma/provenance.rb
|
|
@@ -241,8 +249,10 @@ files:
|
|
|
241
249
|
- lib/suma/schema_naming.rb
|
|
242
250
|
- lib/suma/schema_profile.rb
|
|
243
251
|
- lib/suma/schema_template.rb
|
|
252
|
+
- lib/suma/schema_template/anchors.rb
|
|
244
253
|
- lib/suma/schema_template/document.rb
|
|
245
254
|
- lib/suma/schema_template/plain.rb
|
|
255
|
+
- lib/suma/site_build.rb
|
|
246
256
|
- lib/suma/site_config.rb
|
|
247
257
|
- lib/suma/staged_collection_builder.rb
|
|
248
258
|
- lib/suma/svg_quality.rb
|
|
@@ -256,6 +266,7 @@ files:
|
|
|
256
266
|
- lib/suma/term_classification.rb
|
|
257
267
|
- lib/suma/term_extractor.rb
|
|
258
268
|
- lib/suma/thor_ext.rb
|
|
269
|
+
- lib/suma/translation_ingester.rb
|
|
259
270
|
- lib/suma/urn.rb
|
|
260
271
|
- lib/suma/urn_resolver.rb
|
|
261
272
|
- lib/suma/utils.rb
|