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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SvgQuality
|
|
5
|
+
# Simple report object wrapping svg_conform ValidationResult
|
|
6
|
+
class Report
|
|
7
|
+
attr_reader :file_path, :error_count, :errors
|
|
8
|
+
|
|
9
|
+
def initialize(file_path, validation_result)
|
|
10
|
+
@file_path = file_path
|
|
11
|
+
@validation_result = validation_result
|
|
12
|
+
@error_count = validation_result&.error_count || 0
|
|
13
|
+
@errors = validation_result&.errors || []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
@validation_result&.valid? || false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def quality_tier
|
|
21
|
+
QualityTiers.for_error_count(@error_count)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def quality_score
|
|
25
|
+
return 100 if @error_count.zero?
|
|
26
|
+
return 0 if @error_count >= 200
|
|
27
|
+
|
|
28
|
+
[100 - (@error_count * 0.5), 0].max.round
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def errors_by_severity
|
|
32
|
+
{
|
|
33
|
+
critical: @errors.count { |e| e.requirement_id =~ /critical/i },
|
|
34
|
+
high: @errors.count { |e| e.requirement_id =~ /high/i },
|
|
35
|
+
medium: @errors.count { |e| e.requirement_id =~ /medium/i },
|
|
36
|
+
low: @errors.count { |e| e.requirement_id =~ /low/i },
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
file_path: @file_path,
|
|
43
|
+
valid: valid?,
|
|
44
|
+
error_count: @error_count,
|
|
45
|
+
quality_score: quality_score,
|
|
46
|
+
quality_tier: quality_tier[:name],
|
|
47
|
+
errors_by_severity: errors_by_severity,
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "svg_conform"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module SvgQuality
|
|
7
|
+
# Quality levels computed from error count
|
|
8
|
+
module QualityTiers
|
|
9
|
+
CRITICAL = { name: :critical, min_errors: 200, emoji: "💥" }.freeze
|
|
10
|
+
HIGH = { name: :high, min_errors: 100, emoji: "🔴" }.freeze
|
|
11
|
+
MEDIUM = { name: :medium, min_errors: 50, emoji: "⚠️" }.freeze
|
|
12
|
+
LOW = { name: :low, min_errors: 20, emoji: "🔶" }.freeze
|
|
13
|
+
MINOR = { name: :minor, min_errors: 0, emoji: "✅" }.freeze
|
|
14
|
+
|
|
15
|
+
ALL = [CRITICAL, HIGH, MEDIUM, LOW, MINOR].freeze
|
|
16
|
+
|
|
17
|
+
def self.for_error_count(count)
|
|
18
|
+
ALL.find { |tier| count >= tier[:min_errors] } || MINOR
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def validator(profile: :svg_1_2_rfc)
|
|
24
|
+
SvgConform::Validator.new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "utils"
|
|
5
|
+
require "expressir"
|
|
6
|
+
require "glossarist"
|
|
7
|
+
|
|
8
|
+
module Suma
|
|
9
|
+
class TermExtractor
|
|
10
|
+
REDUNDANT_NOTE_REGEX =
|
|
11
|
+
%r{
|
|
12
|
+
^An? # Starts with "A" or "An"
|
|
13
|
+
\s.*?\sis\sa\stype\sof # Text followed by "is a type of"
|
|
14
|
+
(\sa|\san)? # Optional " a" or " an"
|
|
15
|
+
\s\{\{[^\}]*\}\} # Text in double curly braces
|
|
16
|
+
\s*?\.?$ # Optional whitespace and period at the end
|
|
17
|
+
}x
|
|
18
|
+
|
|
19
|
+
def initialize(schema_manifest_file, output_path, language_code: "eng")
|
|
20
|
+
@schema_manifest_file = File.expand_path(schema_manifest_file)
|
|
21
|
+
@output_path = output_path
|
|
22
|
+
@language_code = language_code
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
get_exp_files.map do |exp_file|
|
|
27
|
+
extract(exp_file)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def get_exp_files
|
|
34
|
+
config = Expressir::SchemaManifest.from_file(@schema_manifest_file)
|
|
35
|
+
paths = config.schemas.map(&:path)
|
|
36
|
+
|
|
37
|
+
if paths.empty?
|
|
38
|
+
raise Errno::ENOENT, "No EXPRESS files found in `#{@schema_manifest_file}`."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
paths
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract(exp_file)
|
|
45
|
+
exp_path_rel = Pathname.new(exp_file).relative_path_from(Pathname.getwd)
|
|
46
|
+
Utils.log "Building terms: #{exp_path_rel}"
|
|
47
|
+
|
|
48
|
+
repo = Expressir::Express::Parser.from_file(exp_file)
|
|
49
|
+
schema = repo.schemas.first
|
|
50
|
+
|
|
51
|
+
raise Error, "Schema must have an associated file" unless schema.file
|
|
52
|
+
|
|
53
|
+
collection = build_managed_concept_collection(schema)
|
|
54
|
+
output_data(collection)
|
|
55
|
+
collection
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def output_data(collection)
|
|
59
|
+
FileUtils.mkdir_p(File.expand_path(@output_path)) unless File.exist?(@output_path)
|
|
60
|
+
Utils.log "Saving collection to files in: #{@output_path}"
|
|
61
|
+
collection.save_to_files(File.expand_path(@output_path))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_managed_concept_collection(schema)
|
|
65
|
+
source_ref = get_source_ref(schema)
|
|
66
|
+
|
|
67
|
+
Glossarist::ManagedConceptCollection.new.tap do |collection|
|
|
68
|
+
schema.entities.each do |entity|
|
|
69
|
+
localized_concept = build_localized_concept(
|
|
70
|
+
schema: schema,
|
|
71
|
+
entity: entity,
|
|
72
|
+
source_ref: source_ref,
|
|
73
|
+
)
|
|
74
|
+
localized_concept_id = get_localized_concept_identifier(schema, entity)
|
|
75
|
+
|
|
76
|
+
managed_data = Glossarist::ManagedConceptData.new.tap do |data|
|
|
77
|
+
data.id = get_entity_identifier(schema, entity)
|
|
78
|
+
data.localizations[@language_code] = localized_concept
|
|
79
|
+
data.localized_concepts = { @language_code => localized_concept_id }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
managed_concept = Glossarist::ManagedConcept.new.tap do |concept|
|
|
83
|
+
concept.id = get_entity_identifier(schema, entity)
|
|
84
|
+
concept.uuid = concept.id
|
|
85
|
+
concept.data = managed_data
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
collection.store(managed_concept)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_localized_concept(schema:, entity:, source_ref:)
|
|
94
|
+
schema_domain = get_domain(schema)
|
|
95
|
+
|
|
96
|
+
localized_concept_data = Glossarist::ConceptData.new.tap do |data|
|
|
97
|
+
data.terms = get_entity_terms(entity)
|
|
98
|
+
data.definition = get_entity_definitions(entity, schema)
|
|
99
|
+
data.language_code = @language_code
|
|
100
|
+
data.domain = schema_domain
|
|
101
|
+
data.sources = [source_ref] if source_ref
|
|
102
|
+
|
|
103
|
+
notes = get_entity_notes(entity, schema_domain, data.definition)
|
|
104
|
+
data.notes = notes if notes && !notes.empty?
|
|
105
|
+
data.examples = []
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Glossarist::LocalizedConcept.new.tap { |c| c.data = localized_concept_data }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def get_entity_identifier(schema, entity)
|
|
112
|
+
"#{schema.id}.#{entity.id}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def get_localized_concept_identifier(schema, entity)
|
|
116
|
+
"#{schema.id}.#{entity.id}-#{@language_code}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def get_source_ref(schema)
|
|
120
|
+
origin = Glossarist::Citation.new.tap do |citation|
|
|
121
|
+
citation.ref = "ISO 10303"
|
|
122
|
+
custom_locality = build_custom_locality(schema)
|
|
123
|
+
citation.custom_locality = custom_locality unless custom_locality.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Glossarist::ConceptSource.new(type: "authoritative", origin: origin)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_custom_locality(schema)
|
|
130
|
+
localities = []
|
|
131
|
+
localities << Glossarist::CustomLocality.new(name: "schema", value: schema.id)
|
|
132
|
+
|
|
133
|
+
version_item = schema.version.items.detect { |i| i.name == "version" }
|
|
134
|
+
if version_item
|
|
135
|
+
localities << Glossarist::CustomLocality.new(name: "version", value: version_item.value)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
localities
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def get_domain(schema)
|
|
142
|
+
prefix = if schema.id.end_with?("_arm", "_mim")
|
|
143
|
+
"application module"
|
|
144
|
+
else
|
|
145
|
+
"resource"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
"#{prefix}: #{schema.id}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def get_entity_terms(entity)
|
|
152
|
+
[
|
|
153
|
+
Glossarist::Designation::Base.new(
|
|
154
|
+
designation: entity.id,
|
|
155
|
+
type: "expression",
|
|
156
|
+
normative_status: "preferred",
|
|
157
|
+
),
|
|
158
|
+
]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def get_entity_definitions(entity, schema)
|
|
162
|
+
schema_type = extract_file_type(schema.file)
|
|
163
|
+
schema_domain = get_domain(schema)
|
|
164
|
+
|
|
165
|
+
definition = generate_entity_definition(entity, schema_domain, schema_type)
|
|
166
|
+
[Glossarist::DetailedDefinition.new(content: definition)]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def get_entity_notes(entity, schema_domain, definitions)
|
|
170
|
+
notes = []
|
|
171
|
+
|
|
172
|
+
if entity.remarks && !entity.remarks.empty?
|
|
173
|
+
trimmed_def = trim_definition(entity.remarks)
|
|
174
|
+
if trimmed_def && !trimmed_def.empty?
|
|
175
|
+
notes << Glossarist::DetailedDefinition.new(
|
|
176
|
+
content: convert_express_xref(trimmed_def, schema_domain),
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
notes = only_keep_first_sentence(notes)
|
|
182
|
+
notes = remove_see_content(notes)
|
|
183
|
+
notes = remove_redundant_note(notes)
|
|
184
|
+
notes = remove_invalid_references(notes)
|
|
185
|
+
compare_with_definitions(notes, definitions)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def only_keep_first_sentence(notes)
|
|
189
|
+
notes.each do |note|
|
|
190
|
+
if note&.content && should_preserve_complete_structure?(note.content)
|
|
191
|
+
next
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if note&.content
|
|
195
|
+
new_content = note.content
|
|
196
|
+
.split(".\n").first.strip
|
|
197
|
+
.split(". ").first.strip
|
|
198
|
+
note.content = new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def should_preserve_complete_structure?(content)
|
|
204
|
+
return false if content.nil? || content.empty?
|
|
205
|
+
|
|
206
|
+
lines = content.split("\n")
|
|
207
|
+
first_paragraph = lines.first&.strip
|
|
208
|
+
|
|
209
|
+
if first_paragraph&.end_with?(":") && lines.length > 1
|
|
210
|
+
if first_paragraph.count(".").positive?
|
|
211
|
+
return false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
remaining_content = lines[1..].join("\n")
|
|
215
|
+
return starts_with_list?(remaining_content.strip)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def compare_with_definitions(notes, definitions)
|
|
222
|
+
if notes&.first&.content == definitions&.first&.content
|
|
223
|
+
return []
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
notes
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def remove_invalid_references(notes)
|
|
230
|
+
notes.reject do |note|
|
|
231
|
+
note.content.include?("image::") ||
|
|
232
|
+
note.content.match?(/<<(.*?){1,999}>>/)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def remove_redundant_note(notes)
|
|
237
|
+
notes.reject do |note|
|
|
238
|
+
note.content.match?(REDUNDANT_NOTE_REGEX) &&
|
|
239
|
+
!note.content.include?("\n")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def remove_see_content(notes)
|
|
244
|
+
notes.each do |note|
|
|
245
|
+
note.content = note.content.gsub(/\s+\(see(.*?){1,999}\)/, "")
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def extract_file_type(filename)
|
|
250
|
+
match = filename.match(/(arm|mim|bom)_annotated\.exp$/)
|
|
251
|
+
return "resource" unless match
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
"arm" => "module_arm",
|
|
255
|
+
"mim" => "module_mim",
|
|
256
|
+
"bom" => "business_object_model",
|
|
257
|
+
}[match.captures[0]] || "resource"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def starts_with_list?(content)
|
|
261
|
+
return false if content.nil? || content.empty?
|
|
262
|
+
|
|
263
|
+
content.match?(/^\s*[*\-+]\s+/) || content.match?(/^\s*\d+\.\s+/)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def trim_definition(definition)
|
|
267
|
+
return nil if definition.nil? || definition.empty?
|
|
268
|
+
|
|
269
|
+
definition_str = definition.is_a?(Array) ? definition.join("\n\n") : definition.to_s
|
|
270
|
+
|
|
271
|
+
return nil if definition_str.empty?
|
|
272
|
+
|
|
273
|
+
paragraphs = definition_str.split("\n\n")
|
|
274
|
+
first_paragraph = paragraphs.first
|
|
275
|
+
|
|
276
|
+
combined = if paragraphs.length == 1
|
|
277
|
+
apply_first_sentence_logic(first_paragraph)
|
|
278
|
+
elsif first_paragraph.end_with?(":") && paragraphs.length > 1 && starts_with_list?(paragraphs[1])
|
|
279
|
+
complete_list = extract_complete_list(paragraphs, 1)
|
|
280
|
+
"#{first_paragraph}\n\n#{complete_list}"
|
|
281
|
+
else
|
|
282
|
+
apply_first_sentence_logic(first_paragraph)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
combined = "#{combined}\n"
|
|
286
|
+
combined.gsub!(/\n\/\/.*?\n/, "\n")
|
|
287
|
+
combined.strip!
|
|
288
|
+
|
|
289
|
+
express_reference_to_mention(combined)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def apply_first_sentence_logic(paragraph)
|
|
293
|
+
new_content = paragraph
|
|
294
|
+
.split(".\n").first.strip
|
|
295
|
+
.split(". ").first.strip
|
|
296
|
+
|
|
297
|
+
new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def extract_complete_list(paragraphs, start_index)
|
|
301
|
+
return paragraphs[start_index] if start_index >= paragraphs.length
|
|
302
|
+
|
|
303
|
+
combined = paragraphs[start_index].dup
|
|
304
|
+
current_index = start_index + 1
|
|
305
|
+
in_continuation_block = combined.include?("--") && !combined.match?(/--.*--/m)
|
|
306
|
+
|
|
307
|
+
while current_index < paragraphs.length
|
|
308
|
+
next_para = paragraphs[current_index]
|
|
309
|
+
|
|
310
|
+
if next_para.match?(/^--\s*$/) || next_para.end_with?("--")
|
|
311
|
+
in_continuation_block = !in_continuation_block
|
|
312
|
+
combined += "\n\n#{next_para}"
|
|
313
|
+
current_index += 1
|
|
314
|
+
next
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if in_continuation_block
|
|
318
|
+
combined += "\n\n#{next_para}"
|
|
319
|
+
current_index += 1
|
|
320
|
+
next
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
if starts_with_list?(next_para) || is_list_continuation?(next_para)
|
|
324
|
+
combined += "\n\n#{next_para}"
|
|
325
|
+
current_index += 1
|
|
326
|
+
in_continuation_block = true if next_para.include?("--") && !next_para.match?(/--.*--/m)
|
|
327
|
+
else
|
|
328
|
+
break
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
combined
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def is_list_continuation?(content)
|
|
336
|
+
return false if content.nil? || content.empty?
|
|
337
|
+
|
|
338
|
+
content.match?(/^\+\s*$/) ||
|
|
339
|
+
content.match?(/^--\s*$/) ||
|
|
340
|
+
content.match?(/^\s{2,}/) ||
|
|
341
|
+
content.start_with?("which", "where", "that")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def express_reference_to_mention(description)
|
|
345
|
+
description
|
|
346
|
+
.gsub(/<<express:([^,]+)>>/) do |_match|
|
|
347
|
+
"{{#{Regexp.last_match[1].split('.').last}}}"
|
|
348
|
+
end.gsub(/<<express:([^,]+),([^>]+)>>/) do |_match|
|
|
349
|
+
"{{#{Regexp.last_match[1].split('.').last}," \
|
|
350
|
+
"#{Regexp.last_match[2]}}}"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def entity_name_to_text(entity_id)
|
|
355
|
+
entity_id.downcase.gsub("_", " ")
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def generate_entity_definition(entity, _domain, schema_type)
|
|
359
|
+
return "" if entity.nil?
|
|
360
|
+
|
|
361
|
+
entity_type = case schema_type
|
|
362
|
+
when "module_arm"
|
|
363
|
+
"{{application object}}"
|
|
364
|
+
when "module_mim"
|
|
365
|
+
"{{entity data type}}"
|
|
366
|
+
when "resource", "business_object_model"
|
|
367
|
+
"{{entity data type}}"
|
|
368
|
+
else
|
|
369
|
+
raise Error, "[suma] encountered unsupported schema_type"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if entity.subtype_of.empty?
|
|
373
|
+
"#{entity_type} " \
|
|
374
|
+
"that represents the " \
|
|
375
|
+
"#{entity_name_to_text(entity.id)} {{entity}}"
|
|
376
|
+
else
|
|
377
|
+
entity_subtypes = entity.subtype_of.map { |e| "{{#{e.id}}}" }
|
|
378
|
+
|
|
379
|
+
"#{entity_type} that is a type of " \
|
|
380
|
+
"#{entity_subtypes.join(' and ')} " \
|
|
381
|
+
"that represents the " \
|
|
382
|
+
"#{entity_name_to_text(entity.id)} {{entity}}"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def convert_express_xref(content, schema_domain)
|
|
387
|
+
content.gsub(/<<express:(.*),(.*)>>/) do
|
|
388
|
+
"{{<#{schema_domain}>" \
|
|
389
|
+
"#{Regexp.last_match(1).split('.').last},#{Regexp.last_match(2)}}}"
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
data/lib/suma/utils.rb
CHANGED
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
module Suma
|
|
4
4
|
module Utils
|
|
5
5
|
class << self
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
attr_writer :output
|
|
7
|
+
|
|
8
|
+
def output
|
|
9
|
+
@output ||= $stderr
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def log(message, level: :info)
|
|
13
|
+
return if level == :debug && !ENV["SUMA_DEBUG"]
|
|
14
|
+
|
|
15
|
+
output.puts "[suma] #{message}"
|
|
8
16
|
end
|
|
9
17
|
end
|
|
10
18
|
end
|
data/lib/suma/version.rb
CHANGED
data/lib/suma.rb
CHANGED
data/suma.gemspec
CHANGED
|
@@ -34,12 +34,13 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
|
34
34
|
spec.require_paths = ["lib"]
|
|
35
35
|
|
|
36
36
|
spec.add_dependency "expressir", ">= 2.1.29", "~> 2.1"
|
|
37
|
-
spec.add_dependency "glossarist", "~> 2.4
|
|
38
|
-
spec.add_dependency "lutaml-model", "~> 0.
|
|
37
|
+
spec.add_dependency "glossarist", "~> 2.4"
|
|
38
|
+
spec.add_dependency "lutaml-model", "~> 0.8.0"
|
|
39
39
|
spec.add_dependency "metanorma", "~> 2.3"
|
|
40
40
|
spec.add_dependency "plurimath"
|
|
41
41
|
spec.add_dependency "ruby-progressbar"
|
|
42
42
|
spec.add_dependency "rubyzip", "~> 2.3"
|
|
43
|
+
spec.add_dependency "svg_conform", "~> 0.1.0"
|
|
43
44
|
spec.add_dependency "table_tennis"
|
|
44
45
|
spec.add_dependency "thor", ">= 0.20"
|
|
45
46
|
|
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.2.
|
|
4
|
+
version: 0.2.6
|
|
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-
|
|
11
|
+
date: 2026-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: expressir
|
|
@@ -36,28 +36,28 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 2.4
|
|
39
|
+
version: '2.4'
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 2.4
|
|
46
|
+
version: '2.4'
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: lutaml-model
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version:
|
|
53
|
+
version: 0.8.0
|
|
54
54
|
type: :runtime
|
|
55
55
|
prerelease: false
|
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
60
|
+
version: 0.8.0
|
|
61
61
|
- !ruby/object:Gem::Dependency
|
|
62
62
|
name: metanorma
|
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -114,6 +114,20 @@ dependencies:
|
|
|
114
114
|
- - "~>"
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
116
|
version: '2.3'
|
|
117
|
+
- !ruby/object:Gem::Dependency
|
|
118
|
+
name: svg_conform
|
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: 0.1.0
|
|
124
|
+
type: :runtime
|
|
125
|
+
prerelease: false
|
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 0.1.0
|
|
117
131
|
- !ruby/object:Gem::Dependency
|
|
118
132
|
name: table_tennis
|
|
119
133
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -158,6 +172,7 @@ files:
|
|
|
158
172
|
- ".rspec"
|
|
159
173
|
- ".rubocop.yml"
|
|
160
174
|
- ".rubocop_todo.yml"
|
|
175
|
+
- CLAUDE.md
|
|
161
176
|
- CODE_OF_CONDUCT.md
|
|
162
177
|
- Gemfile
|
|
163
178
|
- README.adoc
|
|
@@ -168,6 +183,7 @@ files:
|
|
|
168
183
|
- lib/suma.rb
|
|
169
184
|
- lib/suma/cli.rb
|
|
170
185
|
- lib/suma/cli/build.rb
|
|
186
|
+
- lib/suma/cli/check_svg_quality.rb
|
|
171
187
|
- lib/suma/cli/compare.rb
|
|
172
188
|
- lib/suma/cli/convert_jsdai.rb
|
|
173
189
|
- lib/suma/cli/export.rb
|
|
@@ -181,18 +197,28 @@ files:
|
|
|
181
197
|
- lib/suma/eengine/errors.rb
|
|
182
198
|
- lib/suma/eengine/wrapper.rb
|
|
183
199
|
- lib/suma/eengine_converter.rb
|
|
184
|
-
- lib/suma/export_standalone_schema.rb
|
|
185
200
|
- lib/suma/express_schema.rb
|
|
186
201
|
- lib/suma/jsdai.rb
|
|
187
202
|
- lib/suma/jsdai/figure.rb
|
|
188
203
|
- lib/suma/jsdai/figure_image.rb
|
|
189
204
|
- lib/suma/jsdai/figure_xml.rb
|
|
205
|
+
- lib/suma/link_validator.rb
|
|
190
206
|
- lib/suma/processor.rb
|
|
191
207
|
- lib/suma/schema_attachment.rb
|
|
192
208
|
- lib/suma/schema_collection.rb
|
|
209
|
+
- lib/suma/schema_comparer.rb
|
|
193
210
|
- lib/suma/schema_document.rb
|
|
194
211
|
- lib/suma/schema_exporter.rb
|
|
212
|
+
- lib/suma/schema_index.rb
|
|
213
|
+
- lib/suma/schema_manifest_generator.rb
|
|
195
214
|
- lib/suma/site_config.rb
|
|
215
|
+
- lib/suma/svg_quality.rb
|
|
216
|
+
- lib/suma/svg_quality/batch_report.rb
|
|
217
|
+
- lib/suma/svg_quality/formatters/json_formatter.rb
|
|
218
|
+
- lib/suma/svg_quality/formatters/terminal_formatter.rb
|
|
219
|
+
- lib/suma/svg_quality/formatters/yaml_formatter.rb
|
|
220
|
+
- lib/suma/svg_quality/report.rb
|
|
221
|
+
- lib/suma/term_extractor.rb
|
|
196
222
|
- lib/suma/thor_ext.rb
|
|
197
223
|
- lib/suma/utils.rb
|
|
198
224
|
- lib/suma/version.rb
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Suma
|
|
4
|
-
# Simple schema class for standalone EXPRESS files
|
|
5
|
-
# Used when exporting individual .exp files that are not part of a manifest
|
|
6
|
-
class ExportStandaloneSchema
|
|
7
|
-
attr_accessor :id, :path
|
|
8
|
-
|
|
9
|
-
def initialize(id:, path:)
|
|
10
|
-
@id = id
|
|
11
|
-
@path = path
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|