suma 0.2.5 → 0.2.6
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/rake.yml +3 -0
- data/.github/workflows/release.yml +5 -1
- data/.rubocop_todo.yml +78 -26
- data/CLAUDE.md +76 -0
- data/Gemfile +3 -1
- data/README.adoc +131 -0
- data/lib/suma/cli/build.rb +2 -3
- data/lib/suma/cli/check_svg_quality.rb +178 -0
- data/lib/suma/cli/compare.rb +7 -158
- data/lib/suma/cli/export.rb +1 -7
- data/lib/suma/cli/extract_terms.rb +7 -648
- data/lib/suma/cli/generate_schemas.rb +9 -123
- data/lib/suma/cli/validate_links.rb +15 -290
- data/lib/suma/cli.rb +39 -0
- data/lib/suma/collection_manifest.rb +3 -4
- data/lib/suma/express_schema.rb +43 -30
- data/lib/suma/jsdai/figure_xml.rb +12 -9
- data/lib/suma/jsdai.rb +0 -6
- data/lib/suma/link_validator.rb +203 -0
- data/lib/suma/processor.rb +75 -101
- data/lib/suma/schema_attachment.rb +2 -29
- data/lib/suma/schema_collection.rb +1 -32
- data/lib/suma/schema_comparer.rb +116 -0
- data/lib/suma/schema_document.rb +0 -14
- data/lib/suma/schema_exporter.rb +16 -28
- data/lib/suma/schema_index.rb +53 -0
- data/lib/suma/schema_manifest_generator.rb +105 -0
- data/lib/suma/svg_quality/batch_report.rb +80 -0
- data/lib/suma/svg_quality/formatters/json_formatter.rb +30 -0
- data/lib/suma/svg_quality/formatters/terminal_formatter.rb +168 -0
- data/lib/suma/svg_quality/formatters/yaml_formatter.rb +32 -0
- data/lib/suma/svg_quality/report.rb +52 -0
- data/lib/suma/svg_quality.rb +28 -0
- data/lib/suma/term_extractor.rb +393 -0
- data/lib/suma/utils.rb +10 -2
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +3 -2
- data/suma.gemspec +3 -2
- metadata +33 -7
- data/lib/suma/export_standalone_schema.rb +0 -14
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "thor"
|
|
2
4
|
require_relative "../thor_ext"
|
|
3
|
-
|
|
4
|
-
require "yaml"
|
|
5
|
-
require "pathname"
|
|
5
|
+
require_relative "../schema_manifest_generator"
|
|
6
6
|
|
|
7
7
|
module Suma
|
|
8
8
|
module Cli
|
|
9
|
-
# GenerateSchemas command to generate Schemas YAML by Metanorma YAML
|
|
10
9
|
class GenerateSchemas < Thor
|
|
11
10
|
desc "generate_schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE",
|
|
12
11
|
"Generate EXPRESS schema manifest file from Metanorma site manifest"
|
|
@@ -14,125 +13,12 @@ module Suma
|
|
|
14
13
|
desc: "Exclude schemas paths by pattern " \
|
|
15
14
|
"(e.g. `*_lf.exp`)"
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
raise Errno::ENOENT, "Specified file `#{metanorma_manifest_file}` " \
|
|
24
|
-
"not found."
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
unless File.file?(metanorma_manifest_file)
|
|
28
|
-
raise ArgumentError, "Specified path `#{metanorma_manifest_file}` " \
|
|
29
|
-
"is not a file."
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
[metanorma_manifest_file, schema_manifest_file].each do |file|
|
|
33
|
-
if !YAML_FILE_EXTENSIONS.include?(File.extname(file))
|
|
34
|
-
raise ArgumentError, "Specified file `#{file}` is not a YAML file."
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
run(
|
|
39
|
-
metanorma_manifest_file, schema_manifest_file,
|
|
40
|
-
exclude_paths: options[:exclude_paths]
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def run(metanorma_manifest_file, schema_manifest_file, exclude_paths: nil)
|
|
47
|
-
metanorma_data = load_yaml(metanorma_manifest_file)
|
|
48
|
-
collection_files = metanorma_data["metanorma"]["source"]["files"]
|
|
49
|
-
manifest_files = load_manifest_files(collection_files)
|
|
50
|
-
all_schemas = load_project_schemas(manifest_files, exclude_paths,
|
|
51
|
-
schema_manifest_file)
|
|
52
|
-
all_schemas["schemas"] = all_schemas["schemas"].sort.to_h
|
|
53
|
-
output_data(all_schemas, schema_manifest_file)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def output_data(all_schemas, path)
|
|
57
|
-
puts "Writing the Schemas YAML file to #{File.expand_path(path)}..."
|
|
58
|
-
# debug use only
|
|
59
|
-
# puts all_schemas.to_yaml
|
|
60
|
-
File.write(File.expand_path(path), all_schemas.to_yaml)
|
|
61
|
-
puts "Writing the Schemas YAML file to #{File.expand_path(path)}...Done"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def load_yaml(file_path)
|
|
65
|
-
YAML.safe_load(
|
|
66
|
-
File.read(file_path, encoding: "UTF-8"),
|
|
67
|
-
aliases: true,
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def load_manifest_files(collection_files)
|
|
72
|
-
manifest_files = collection_files.map do |c|
|
|
73
|
-
collection_data = load_yaml(c)
|
|
74
|
-
collection_data["manifest"]["docref"].map { |docref| docref["file"] }
|
|
75
|
-
end
|
|
76
|
-
manifest_files.flatten
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def load_project_schemas( # rubocop:disable Metrics/AbcSize
|
|
80
|
-
manifest_files, exclude_paths, schema_manifest_file
|
|
81
|
-
)
|
|
82
|
-
all_schemas = { "schemas" => {} }
|
|
83
|
-
|
|
84
|
-
manifest_files.each do |file|
|
|
85
|
-
# load schemas.yaml from the location of the collection.yml file
|
|
86
|
-
schemas_file_path = File.expand_path(
|
|
87
|
-
file.gsub("collection.yml", "schemas.yaml"),
|
|
88
|
-
)
|
|
89
|
-
unless File.exist?(schemas_file_path)
|
|
90
|
-
puts "Schemas file not found: #{schemas_file_path}"
|
|
91
|
-
next
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
schemas_data = load_yaml(schemas_file_path)
|
|
95
|
-
|
|
96
|
-
if schemas_data["schemas"]
|
|
97
|
-
schemas_data["schemas"] = fix_path(
|
|
98
|
-
schemas_data,
|
|
99
|
-
schemas_file_path,
|
|
100
|
-
schema_manifest_file,
|
|
101
|
-
)
|
|
102
|
-
all_schemas["schemas"].merge!(schemas_data["schemas"])
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
if exclude_paths
|
|
106
|
-
all_schemas["schemas"].delete_if do |_key, value|
|
|
107
|
-
value["path"].match?(
|
|
108
|
-
Regexp.new(exclude_paths.gsub("*", "(.*){1,999}")),
|
|
109
|
-
)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
all_schemas
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def fix_path(schemas_data, schemas_file_path, schema_manifest_file) # rubocop:disable Metrics/AbcSize
|
|
118
|
-
schema_manifest_path = File.expand_path(schema_manifest_file, Dir.pwd)
|
|
119
|
-
|
|
120
|
-
schemas_data["schemas"].each do |key, value|
|
|
121
|
-
# resolve the path in the schema file by the path in the schemas.yaml
|
|
122
|
-
path_in_schema = File.expand_path(
|
|
123
|
-
value["path"],
|
|
124
|
-
File.dirname(schemas_file_path),
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# calculate the relative path from the schema manifest file
|
|
128
|
-
fixed_path = Pathname.new(path_in_schema).relative_path_from(
|
|
129
|
-
Pathname.new(File.dirname(schema_manifest_path)),
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
{ key => value.merge!("path" => fixed_path.to_s) }
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
schemas_data["schemas"]
|
|
16
|
+
def generate_schemas(metanorma_manifest_file, schema_manifest_file)
|
|
17
|
+
SchemaManifestGenerator.new(
|
|
18
|
+
metanorma_manifest_file,
|
|
19
|
+
schema_manifest_file,
|
|
20
|
+
exclude_paths: options[:exclude_paths],
|
|
21
|
+
).generate
|
|
136
22
|
end
|
|
137
23
|
end
|
|
138
24
|
end
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "../utils"
|
|
5
|
+
require_relative "../link_validator"
|
|
5
6
|
require "expressir"
|
|
6
7
|
|
|
7
8
|
module Suma
|
|
8
9
|
module Cli
|
|
9
|
-
# ValidateLinks command for managing EXPRESS links
|
|
10
10
|
class ValidateLinks < Thor
|
|
11
11
|
desc "extract_and_validate SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]",
|
|
12
12
|
"Extract and validate express links without creating intermediate file"
|
|
@@ -16,47 +16,38 @@ module Suma
|
|
|
16
16
|
load_dependencies
|
|
17
17
|
paths = prepare_file_paths(schemas_file, documents_path, output_file)
|
|
18
18
|
|
|
19
|
-
# Load schemas and extract links
|
|
20
19
|
schemas_config = load_schemas_config(paths[:schemas_file])
|
|
21
|
-
exp_files = collect_schema_paths(schemas_config,
|
|
22
|
-
paths[:schemas_file_rel])
|
|
20
|
+
exp_files = collect_schema_paths(schemas_config, paths[:schemas_file_rel])
|
|
23
21
|
adoc_files = find_adoc_files(paths[:documents_path])
|
|
24
22
|
|
|
25
23
|
all_files = adoc_files + exp_files
|
|
26
24
|
display_file_counts(adoc_files, exp_files)
|
|
27
25
|
|
|
28
|
-
# Extract links from files
|
|
29
26
|
links_by_file = extract_links(all_files)
|
|
30
27
|
|
|
31
|
-
# Validate links against schemas
|
|
32
28
|
repo = load_express_schemas(schemas_config)
|
|
33
|
-
|
|
29
|
+
index = SchemaIndex.new(repo)
|
|
30
|
+
unresolved = LinkValidator.new(index).validate(links_by_file)
|
|
34
31
|
|
|
35
|
-
# Generate and output results
|
|
36
32
|
write_validation_results(paths[:output_file], paths[:output_file_rel],
|
|
37
|
-
|
|
33
|
+
unresolved, links_by_file)
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
private
|
|
41
37
|
|
|
42
|
-
# Load all required dependencies for link validation
|
|
43
38
|
def load_dependencies
|
|
44
|
-
# Lazy-load dependencies only when this command is actually used
|
|
45
39
|
require "expressir"
|
|
46
40
|
require "ruby-progressbar"
|
|
47
41
|
require "pathname"
|
|
48
42
|
end
|
|
49
43
|
|
|
50
|
-
# Prepare and normalize all file paths needed for the operation
|
|
51
44
|
def prepare_file_paths(schemas_file, documents_path, output_file)
|
|
52
|
-
# Convert to absolute paths
|
|
53
45
|
schemas_file_path = Pathname.new(schemas_file).expand_path
|
|
54
|
-
|
|
46
|
+
documents_path_exp = Pathname.new(documents_path).expand_path
|
|
55
47
|
output_file_path = Pathname.new(output_file).expand_path
|
|
56
48
|
|
|
57
|
-
# Store relative paths for display
|
|
58
49
|
schemas_file_rel = Pathname.new(schemas_file_path).relative_path_from(Pathname.pwd).to_s
|
|
59
|
-
documents_path_rel = Pathname.new(
|
|
50
|
+
documents_path_rel = Pathname.new(documents_path_exp).relative_path_from(Pathname.pwd).to_s
|
|
60
51
|
output_file_rel = Pathname.new(output_file_path).relative_path_from(Pathname.pwd).to_s
|
|
61
52
|
|
|
62
53
|
puts "Extracting and validating express links using schemas from #{schemas_file_rel}..."
|
|
@@ -65,46 +56,35 @@ module Suma
|
|
|
65
56
|
{
|
|
66
57
|
schemas_file: schemas_file_path,
|
|
67
58
|
schemas_file_rel: schemas_file_rel,
|
|
68
|
-
documents_path:
|
|
59
|
+
documents_path: documents_path_exp,
|
|
69
60
|
documents_path_rel: documents_path_rel,
|
|
70
61
|
output_file: output_file_path,
|
|
71
62
|
output_file_rel: output_file_rel,
|
|
72
63
|
}
|
|
73
64
|
end
|
|
74
65
|
|
|
75
|
-
# Load and initialize the schemas configuration
|
|
76
66
|
def load_schemas_config(schemas_file_path)
|
|
77
67
|
schemas_config = Expressir::SchemaManifest.from_yaml(File.read(schemas_file_path))
|
|
78
|
-
# Ensure the config is initialized with the correct path to resolve relative paths
|
|
79
68
|
schemas_config.set_initial_path(schemas_file_path.to_s)
|
|
80
69
|
schemas_config
|
|
81
70
|
rescue StandardError => e
|
|
82
|
-
|
|
83
|
-
exit(1)
|
|
71
|
+
raise Suma::Error, "Error loading schemas file: #{e.message}"
|
|
84
72
|
end
|
|
85
73
|
|
|
86
|
-
# Collect paths to all schema files from the config
|
|
87
74
|
def collect_schema_paths(schemas_config, schemas_file_rel)
|
|
88
|
-
exp_files =
|
|
89
|
-
schemas_config.schemas.each do |schema|
|
|
90
|
-
exp_files << schema.path if schema.path
|
|
91
|
-
end
|
|
92
|
-
|
|
75
|
+
exp_files = schemas_config.schemas.filter_map(&:path)
|
|
93
76
|
puts "Found #{exp_files.size} EXPRESS schema files from #{schemas_file_rel}"
|
|
94
77
|
exp_files
|
|
95
78
|
end
|
|
96
79
|
|
|
97
|
-
# Find all AsciiDoc files in the specified path
|
|
98
80
|
def find_adoc_files(documents_path)
|
|
99
81
|
Dir.glob(documents_path.join("**", "*.adoc").to_s)
|
|
100
82
|
end
|
|
101
83
|
|
|
102
|
-
# Display counts of discovered files
|
|
103
84
|
def display_file_counts(adoc_files, exp_files)
|
|
104
85
|
puts "Found #{adoc_files.size} AsciiDoc files and #{exp_files.size} EXPRESS files"
|
|
105
86
|
end
|
|
106
87
|
|
|
107
|
-
# Create a standardized progress bar
|
|
108
88
|
def create_progress_bar(title, total)
|
|
109
89
|
ProgressBar.create(
|
|
110
90
|
title: title,
|
|
@@ -116,7 +96,6 @@ module Suma
|
|
|
116
96
|
)
|
|
117
97
|
end
|
|
118
98
|
|
|
119
|
-
# Extract EXPRESS links from all files
|
|
120
99
|
def extract_links(files)
|
|
121
100
|
links_by_file = {}
|
|
122
101
|
link_count = 0
|
|
@@ -127,7 +106,6 @@ module Suma
|
|
|
127
106
|
progress.increment
|
|
128
107
|
begin
|
|
129
108
|
content = File.read(file)
|
|
130
|
-
# Extract links while ignoring text after comma
|
|
131
109
|
express_links = content.scan(/<<express:([^,>]+)(?:,[^>]+)?>>/).flatten.uniq
|
|
132
110
|
|
|
133
111
|
if express_links.any?
|
|
@@ -143,282 +121,31 @@ module Suma
|
|
|
143
121
|
links_by_file
|
|
144
122
|
end
|
|
145
123
|
|
|
146
|
-
# Load all EXPRESS schemas for validation
|
|
147
124
|
def load_express_schemas(schemas_config)
|
|
148
|
-
# Get all schema paths for validation
|
|
149
125
|
schema_paths = {}
|
|
150
|
-
schemas_config.schemas.each
|
|
151
|
-
schema_paths[schema.id] = schema.path
|
|
152
|
-
end
|
|
126
|
+
schemas_config.schemas.each { |s| schema_paths[s.id] = s.path }
|
|
153
127
|
|
|
154
128
|
puts "Loading #{schema_paths.size} EXPRESS schemas for validation..."
|
|
155
129
|
|
|
156
|
-
|
|
157
|
-
loading_progress = create_progress_bar("Loading schemas",
|
|
158
|
-
schema_paths.size)
|
|
130
|
+
loading_progress = create_progress_bar("Loading schemas", schema_paths.size)
|
|
159
131
|
|
|
160
|
-
# Try to load all schemas with progress tracking
|
|
161
132
|
begin
|
|
162
133
|
repo = Expressir::Express::Parser.from_files(schema_paths.values) do |filename, _schemas, error|
|
|
163
134
|
loading_progress.increment
|
|
164
|
-
if error
|
|
165
|
-
puts "\nWarning: Error loading schema #{filename}: #{error.message}"
|
|
166
|
-
end
|
|
135
|
+
puts "\nWarning: Error loading schema #{filename}: #{error.message}" if error
|
|
167
136
|
end
|
|
168
137
|
|
|
169
138
|
puts "Successfully loaded #{repo.schemas.size} schemas"
|
|
170
139
|
repo
|
|
171
140
|
rescue StandardError => e
|
|
172
|
-
|
|
173
|
-
exit(1)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Validate all links against loaded schemas
|
|
178
|
-
def validate_links(links_by_file, repo)
|
|
179
|
-
unresolved_links = []
|
|
180
|
-
total_links = links_by_file.values.sum(&:size)
|
|
181
|
-
|
|
182
|
-
progress = create_progress_bar("Validating links", total_links)
|
|
183
|
-
|
|
184
|
-
links_by_file.each do |file, links|
|
|
185
|
-
validate_file_links(file, links, repo, progress, unresolved_links)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
unresolved_links
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Validate links in a specific file
|
|
192
|
-
def validate_file_links(file, links, repo, progress, unresolved_links)
|
|
193
|
-
file_content = File.read(file)
|
|
194
|
-
file_lines = file_content.lines
|
|
195
|
-
|
|
196
|
-
links.each do |link|
|
|
197
|
-
progress.increment
|
|
198
|
-
line_idx = find_link_line(file_lines, link)
|
|
199
|
-
next unless line_idx
|
|
200
|
-
|
|
201
|
-
# Parse link (schema only, schema.element, or schema.element.path)
|
|
202
|
-
parts = link.split(".")
|
|
203
|
-
|
|
204
|
-
if parts.size == 1
|
|
205
|
-
validate_schema_only_link(parts[0], repo, file, line_idx, link,
|
|
206
|
-
unresolved_links)
|
|
207
|
-
else
|
|
208
|
-
validate_schema_element_link(parts, repo, file, line_idx, link,
|
|
209
|
-
unresolved_links)
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
rescue StandardError => e
|
|
213
|
-
puts "Warning: Error processing file #{file}: #{e.message}"
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Find the line where a link appears in the file
|
|
217
|
-
def find_link_line(file_lines, link)
|
|
218
|
-
file_lines.each_with_index do |line, idx|
|
|
219
|
-
# Match both with and without comma text
|
|
220
|
-
if /<<express:#{Regexp.escape(link)}(?:,[^>]+)?>>/.match?(line)
|
|
221
|
-
return idx
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
nil
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Validate a link that only references a schema
|
|
228
|
-
def validate_schema_only_link(schema_name, repo, file, line_idx, link,
|
|
229
|
-
unresolved_links)
|
|
230
|
-
# Check if schema exists
|
|
231
|
-
schema = repo.schemas.find do |s|
|
|
232
|
-
s.id.downcase == schema_name.downcase
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
if !schema
|
|
236
|
-
unresolved_links << {
|
|
237
|
-
file: file,
|
|
238
|
-
line: line_idx + 1,
|
|
239
|
-
link: link,
|
|
240
|
-
reason: "Schema '#{schema_name}' not found",
|
|
241
|
-
}
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Validate a link with schema.element or deeper paths
|
|
246
|
-
def validate_schema_element_link(parts, repo, file, line_idx, link,
|
|
247
|
-
unresolved_links)
|
|
248
|
-
schema_name = parts[0]
|
|
249
|
-
element_name = parts[1]
|
|
250
|
-
|
|
251
|
-
# Check if schema exists
|
|
252
|
-
schema = repo.schemas.find do |s|
|
|
253
|
-
s.id.downcase == schema_name.downcase
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
if !schema
|
|
257
|
-
unresolved_links << {
|
|
258
|
-
file: file,
|
|
259
|
-
line: line_idx + 1,
|
|
260
|
-
link: link,
|
|
261
|
-
reason: "Schema '#{schema_name}' not found",
|
|
262
|
-
}
|
|
263
|
-
return
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Find the element in the schema
|
|
267
|
-
element = find_schema_element(schema, element_name)
|
|
268
|
-
|
|
269
|
-
if !element
|
|
270
|
-
unresolved_links << {
|
|
271
|
-
file: file,
|
|
272
|
-
line: line_idx + 1,
|
|
273
|
-
link: link,
|
|
274
|
-
reason: "Element '#{element_name}' not found in schema '#{schema_name}'",
|
|
275
|
-
}
|
|
276
|
-
return
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# If we have more than 2 parts, validate the deeper path
|
|
280
|
-
if parts.size > 2
|
|
281
|
-
validation_error = validate_deep_path(schema, element, parts[2..],
|
|
282
|
-
file, line_idx, link)
|
|
283
|
-
unresolved_links << validation_error if validation_error
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Find an element in a schema by name
|
|
288
|
-
def find_schema_element(schema, element_name)
|
|
289
|
-
# Try to find element in various collections
|
|
290
|
-
element_collections = [
|
|
291
|
-
schema.entities,
|
|
292
|
-
schema.types,
|
|
293
|
-
schema.constants,
|
|
294
|
-
schema.functions,
|
|
295
|
-
schema.rules,
|
|
296
|
-
schema.procedures,
|
|
297
|
-
schema.subtype_constraints,
|
|
298
|
-
]
|
|
299
|
-
|
|
300
|
-
element_collections.each do |collection|
|
|
301
|
-
element = collection&.find do |e|
|
|
302
|
-
e.id.downcase == element_name.downcase
|
|
303
|
-
end
|
|
304
|
-
return element if element
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
nil
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Validate deeper paths in a link (schema.element.path.subpath)
|
|
311
|
-
def validate_deep_path(schema, element, path_parts, file, line_idx,
|
|
312
|
-
full_link)
|
|
313
|
-
current_element = element
|
|
314
|
-
current_path = "#{schema.id}.#{element.id}"
|
|
315
|
-
|
|
316
|
-
# Process each part of the path
|
|
317
|
-
path_parts.each do |part|
|
|
318
|
-
# The validation logic depends on the type of current element
|
|
319
|
-
case current_element
|
|
320
|
-
when Expressir::Express::Entity
|
|
321
|
-
# For entities, check attributes
|
|
322
|
-
attribute = current_element.attributes&.find do |a|
|
|
323
|
-
a.id.downcase == part.downcase
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
unless attribute
|
|
327
|
-
return {
|
|
328
|
-
file: file,
|
|
329
|
-
line: line_idx + 1,
|
|
330
|
-
link: full_link,
|
|
331
|
-
reason: "Attribute '#{part}' not found in entity '#{current_path}'",
|
|
332
|
-
}
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
current_element = attribute
|
|
336
|
-
current_path += ".#{part}"
|
|
337
|
-
|
|
338
|
-
when Expressir::Express::Type
|
|
339
|
-
# For types, validation depends on type kind
|
|
340
|
-
if current_element.respond_to?(:base_type) && current_element.base_type
|
|
341
|
-
# For derived types, find the base type
|
|
342
|
-
base_type = find_base_type(schema, current_element.base_type)
|
|
343
|
-
|
|
344
|
-
unless base_type
|
|
345
|
-
return {
|
|
346
|
-
file: file,
|
|
347
|
-
line: line_idx + 1,
|
|
348
|
-
link: full_link,
|
|
349
|
-
reason: "Base type not found for '#{current_path}'",
|
|
350
|
-
}
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Continue validation using base type
|
|
354
|
-
current_element = base_type
|
|
355
|
-
|
|
356
|
-
elsif current_element.respond_to?(:elements) && current_element.elements
|
|
357
|
-
# For enum types, check enum values
|
|
358
|
-
enum_value = current_element.elements.find do |e|
|
|
359
|
-
e.id.downcase == part.downcase
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
unless enum_value
|
|
363
|
-
return {
|
|
364
|
-
file: file,
|
|
365
|
-
line: line_idx + 1,
|
|
366
|
-
link: full_link,
|
|
367
|
-
reason: "Enumeration value '#{part}' not found in type '#{current_path}'",
|
|
368
|
-
}
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
current_element = enum_value
|
|
372
|
-
current_path += ".#{part}"
|
|
373
|
-
|
|
374
|
-
else
|
|
375
|
-
# For other types, we can't navigate deeper
|
|
376
|
-
return {
|
|
377
|
-
file: file,
|
|
378
|
-
line: line_idx + 1,
|
|
379
|
-
link: full_link,
|
|
380
|
-
reason: "Cannot navigate deeper from type '#{current_path}'",
|
|
381
|
-
}
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
else
|
|
385
|
-
# For other element types, navigation is not supported
|
|
386
|
-
return {
|
|
387
|
-
file: file,
|
|
388
|
-
line: line_idx + 1,
|
|
389
|
-
link: full_link,
|
|
390
|
-
reason: "Cannot navigate deeper from '#{current_path}'",
|
|
391
|
-
}
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# If we've processed all parts without returning an error, path is valid
|
|
396
|
-
nil
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
# Find a base type in a schema
|
|
400
|
-
def find_base_type(schema, type_ref)
|
|
401
|
-
# Skip built-in types
|
|
402
|
-
return nil if %w[INTEGER REAL STRING BOOLEAN NUMBER BINARY
|
|
403
|
-
LOGICAL].include?(type_ref.to_s.upcase)
|
|
404
|
-
|
|
405
|
-
# Find the referenced type in the schema
|
|
406
|
-
if type_ref.is_a?(String)
|
|
407
|
-
find_schema_element(schema, type_ref)
|
|
408
|
-
elsif type_ref.respond_to?(:id)
|
|
409
|
-
# It's already a type object
|
|
410
|
-
type_ref
|
|
411
|
-
else
|
|
412
|
-
nil
|
|
141
|
+
raise Suma::Error, "Error loading schemas: #{e.message}"
|
|
413
142
|
end
|
|
414
143
|
end
|
|
415
144
|
|
|
416
|
-
# Write validation results to the output file
|
|
417
145
|
def write_validation_results(output_file_path, output_file_rel,
|
|
418
146
|
unresolved_links, links_by_file)
|
|
419
147
|
total_links = links_by_file.values.sum(&:size)
|
|
420
148
|
|
|
421
|
-
# Prepare results for output
|
|
422
149
|
results = []
|
|
423
150
|
results << "Validation complete. Checked #{total_links} links."
|
|
424
151
|
|
|
@@ -427,17 +154,15 @@ module Suma
|
|
|
427
154
|
else
|
|
428
155
|
results << "❌ Found #{unresolved_links.size} unresolved links:"
|
|
429
156
|
unresolved_links.each do |issue|
|
|
430
|
-
results << "#{issue
|
|
157
|
+
results << "#{issue.file}:#{issue.line} - <<express:#{issue.link}>> - #{issue.reason}"
|
|
431
158
|
end
|
|
432
159
|
end
|
|
433
160
|
|
|
434
|
-
# Write results to output file
|
|
435
161
|
begin
|
|
436
162
|
File.write(output_file_path, results.join("\n"))
|
|
437
163
|
puts "Validation results written to #{output_file_rel}"
|
|
438
164
|
rescue StandardError => e
|
|
439
165
|
puts "Error writing to output file: #{e.message}"
|
|
440
|
-
# Still print results to console as fallback
|
|
441
166
|
puts results
|
|
442
167
|
end
|
|
443
168
|
end
|
data/lib/suma/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "thor_ext"
|
|
5
5
|
require_relative "cli/validate"
|
|
6
|
+
require_relative "cli/check_svg_quality"
|
|
6
7
|
require "expressir"
|
|
7
8
|
require "expressir/cli"
|
|
8
9
|
|
|
@@ -97,6 +98,44 @@ module Suma
|
|
|
97
98
|
desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
|
|
98
99
|
subcommand "validate", Cli::Validate
|
|
99
100
|
|
|
101
|
+
desc "check_svg_quality [PATH]",
|
|
102
|
+
"Check SVG quality and sort by severity (critical files first)"
|
|
103
|
+
option :pattern, type: :string, default: Cli::CheckSvgQuality::DEFAULT_PATTERN,
|
|
104
|
+
desc: "Glob pattern for finding SVG files"
|
|
105
|
+
option :profile, type: :string,
|
|
106
|
+
default: Cli::CheckSvgQuality::DEFAULT_PROFILE,
|
|
107
|
+
desc: "Validation profile to use (metanorma, svg_1_2_rfc, etc.)"
|
|
108
|
+
option :format, type: :string, default: "terminal",
|
|
109
|
+
desc: "Output format: terminal, yaml, json"
|
|
110
|
+
option :output, type: :string, aliases: "-o",
|
|
111
|
+
desc: "Output file path"
|
|
112
|
+
option :min_errors, type: :numeric,
|
|
113
|
+
desc: "Minimum error count threshold"
|
|
114
|
+
option :limit, type: :numeric, default: nil,
|
|
115
|
+
desc: "Maximum number of files to show (default: unlimited)"
|
|
116
|
+
option :sort, type: :string, default: "errors",
|
|
117
|
+
desc: "Sort by: errors (most errors first) or quality (lowest scores first)"
|
|
118
|
+
option :progress, type: :boolean, default: false,
|
|
119
|
+
desc: "Show progress during processing"
|
|
120
|
+
option :summary_only, type: :boolean, default: false,
|
|
121
|
+
desc: "Show only summary"
|
|
122
|
+
def check_svg_quality(path = Cli::CheckSvgQuality::DATA_PATH)
|
|
123
|
+
require_relative "cli/check_svg_quality"
|
|
124
|
+
|
|
125
|
+
analyzer = Cli::CheckSvgQuality.new(
|
|
126
|
+
pattern: options[:pattern],
|
|
127
|
+
profile: options[:profile],
|
|
128
|
+
format: options[:format],
|
|
129
|
+
output: options[:output],
|
|
130
|
+
min_errors: options[:min_errors],
|
|
131
|
+
summary_only: options[:summary_only],
|
|
132
|
+
progress: options[:progress],
|
|
133
|
+
limit: options[:limit],
|
|
134
|
+
sort: options[:sort],
|
|
135
|
+
)
|
|
136
|
+
analyzer.run(path)
|
|
137
|
+
end
|
|
138
|
+
|
|
100
139
|
desc "expressir SUBCOMMAND ...ARGS", "Expressir commands"
|
|
101
140
|
subcommand "expressir", Expressir::Cli
|
|
102
141
|
|
|
@@ -8,7 +8,6 @@ module Suma
|
|
|
8
8
|
attribute :schemas_only, Lutaml::Model::Type::Boolean
|
|
9
9
|
attribute :entry, CollectionManifest, collection: true,
|
|
10
10
|
initialize_empty: true
|
|
11
|
-
# attribute :schema_source, Lutaml::Model::Type::String
|
|
12
11
|
attr_accessor :schema_config
|
|
13
12
|
|
|
14
13
|
yaml do
|
|
@@ -59,9 +58,9 @@ module Suma
|
|
|
59
58
|
export_config
|
|
60
59
|
end
|
|
61
60
|
|
|
62
|
-
def
|
|
63
|
-
results = entry.select
|
|
64
|
-
results << self if
|
|
61
|
+
def lookup_schemas_only
|
|
62
|
+
results = entry.select(&:schemas_only)
|
|
63
|
+
results << self if schemas_only
|
|
65
64
|
results
|
|
66
65
|
end
|
|
67
66
|
|