suma 0.3.0 → 0.5.0
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/.gitignore +2 -5
- data/README.adoc +70 -12
- data/lib/suma/cli/check_svg_quality.rb +82 -91
- data/lib/suma/cli/core.rb +32 -4
- data/lib/suma/cli/export.rb +37 -19
- data/lib/suma/cli/reformat.rb +39 -63
- data/lib/suma/cli/validate.rb +14 -5
- data/lib/suma/cli/validate_links.rb +11 -155
- data/lib/suma/eengine_converter.rb +9 -8
- data/lib/suma/express_reformatter.rb +94 -0
- data/lib/suma/link_validation.rb +144 -0
- data/lib/suma/link_validator/step.rb +149 -0
- data/lib/suma/link_validator.rb +22 -98
- data/lib/suma/note_processor.rb +270 -0
- data/lib/suma/schema_collection.rb +1 -1
- data/lib/suma/schema_comparer.rb +1 -1
- data/lib/suma/schema_exporter.rb +14 -26
- data/lib/suma/svg_quality/scanner.rb +61 -0
- data/lib/suma/svg_quality.rb +1 -0
- data/lib/suma/term_classification.rb +78 -0
- data/lib/suma/term_extractor.rb +25 -240
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +5 -0
- metadata +8 -2
data/lib/suma/link_validator.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "expressir"
|
|
4
|
+
require "suma/link_validation"
|
|
4
5
|
|
|
5
6
|
module Suma
|
|
6
7
|
LinkValidationResult = Struct.new(:file, :line, :link, :reason,
|
|
7
8
|
keyword_init: true)
|
|
8
9
|
|
|
9
10
|
class LinkValidator
|
|
11
|
+
autoload :Step, "suma/link_validator/step"
|
|
12
|
+
|
|
10
13
|
def initialize(index)
|
|
11
14
|
@index = index
|
|
12
15
|
end
|
|
@@ -28,7 +31,7 @@ module Suma
|
|
|
28
31
|
content = File.read(file)
|
|
29
32
|
index = {}
|
|
30
33
|
content.lines.each_with_index do |line, idx|
|
|
31
|
-
line.scan(
|
|
34
|
+
line.scan(LinkValidation::EXPRESS_LINK_PATTERN).flatten.each do |link|
|
|
32
35
|
index[link] ||= idx
|
|
33
36
|
end
|
|
34
37
|
end
|
|
@@ -99,113 +102,34 @@ module Suma
|
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def validate_deep_path(schema, element, path_parts, file, line_idx,
|
|
102
|
-
full_link)
|
|
105
|
+
full_link)
|
|
103
106
|
current = element
|
|
104
107
|
current_path = "#{schema.id}.#{element.id}"
|
|
108
|
+
context = Step::Context.new(file: file, line: line_idx, link: full_link,
|
|
109
|
+
schema: schema, path: current_path)
|
|
105
110
|
|
|
106
111
|
path_parts.each do |part|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
attribute = current.attributes&.find do |a|
|
|
110
|
-
a.id.downcase == part.downcase
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
unless attribute
|
|
114
|
-
return LinkValidationResult.new(
|
|
115
|
-
file: file,
|
|
116
|
-
line: line_idx + 1,
|
|
117
|
-
link: full_link,
|
|
118
|
-
reason: "Attribute '#{part}' not found in entity '#{current_path}'",
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
current = attribute
|
|
123
|
-
current_path += ".#{part}"
|
|
124
|
-
|
|
125
|
-
when Expressir::Model::Declarations::Type
|
|
126
|
-
underlying = current.underlying_type
|
|
127
|
-
|
|
128
|
-
if underlying.is_a?(Expressir::Model::DataTypes::Enumeration)
|
|
129
|
-
enum_value = underlying.items.find do |e|
|
|
130
|
-
e.id.downcase == part.downcase
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
unless enum_value
|
|
134
|
-
return LinkValidationResult.new(
|
|
135
|
-
file: file,
|
|
136
|
-
line: line_idx + 1,
|
|
137
|
-
link: full_link,
|
|
138
|
-
reason: "Enumeration value '#{part}' not found in type '#{current_path}'",
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
current = enum_value
|
|
143
|
-
current_path += ".#{part}"
|
|
144
|
-
|
|
145
|
-
elsif underlying
|
|
146
|
-
base_type = find_base_type(schema, underlying)
|
|
147
|
-
|
|
148
|
-
unless base_type
|
|
149
|
-
return LinkValidationResult.new(
|
|
150
|
-
file: file,
|
|
151
|
-
line: line_idx + 1,
|
|
152
|
-
link: full_link,
|
|
153
|
-
reason: "Base type not found for '#{current_path}'",
|
|
154
|
-
)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
current = base_type
|
|
158
|
-
|
|
159
|
-
else
|
|
160
|
-
return LinkValidationResult.new(
|
|
161
|
-
file: file,
|
|
162
|
-
line: line_idx + 1,
|
|
163
|
-
link: full_link,
|
|
164
|
-
reason: "Cannot navigate deeper from type '#{current_path}'",
|
|
165
|
-
)
|
|
166
|
-
end
|
|
112
|
+
step = Step.for(current)
|
|
113
|
+
return unsupported_step_failure(context) unless step
|
|
167
114
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
end
|
|
115
|
+
outcome = step.new.navigate(current, part, context)
|
|
116
|
+
return outcome if outcome.is_a?(LinkValidationResult)
|
|
117
|
+
|
|
118
|
+
current = outcome
|
|
119
|
+
current_path += ".#{part}"
|
|
120
|
+
context.path = current_path
|
|
176
121
|
end
|
|
177
122
|
|
|
178
123
|
nil
|
|
179
124
|
end
|
|
180
125
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
type_ref
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def find_schema_element(schema, element_name)
|
|
193
|
-
[
|
|
194
|
-
schema.entities,
|
|
195
|
-
schema.types,
|
|
196
|
-
schema.constants,
|
|
197
|
-
schema.functions,
|
|
198
|
-
schema.rules,
|
|
199
|
-
schema.procedures,
|
|
200
|
-
schema.subtype_constraints,
|
|
201
|
-
].each do |collection|
|
|
202
|
-
element = collection&.find do |e|
|
|
203
|
-
e.id.downcase == element_name.downcase
|
|
204
|
-
end
|
|
205
|
-
return element if element
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
nil
|
|
126
|
+
def unsupported_step_failure(context)
|
|
127
|
+
LinkValidationResult.new(
|
|
128
|
+
file: context.file,
|
|
129
|
+
line: context.line + 1,
|
|
130
|
+
link: context.link,
|
|
131
|
+
reason: "Cannot navigate deeper from '#{context.path}'",
|
|
132
|
+
)
|
|
209
133
|
end
|
|
210
134
|
end
|
|
211
135
|
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "glossarist"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
# Cleans raw EXPRESS entity remarks into a list of
|
|
7
|
+
# +Glossarist::V3::DetailedDefinition+ notes.
|
|
8
|
+
#
|
|
9
|
+
# Pipeline:
|
|
10
|
+
# 1. trim to first sentence / first complete list
|
|
11
|
+
# 2. convert express xrefs to URN mentions
|
|
12
|
+
# 3. drop redundant "X is a type of {{...}}" opening notes
|
|
13
|
+
# 4. drop notes containing image references or <<...>> blocks
|
|
14
|
+
# 5. drop notes that duplicate the first definition
|
|
15
|
+
#
|
|
16
|
+
# Pure: no I/O, no schema knowledge. The schema domain label and
|
|
17
|
+
# URN composition are injected via +xref_to_mention+ so this module
|
|
18
|
+
# stays independent of +Suma::Urn+ and the surrounding extractor.
|
|
19
|
+
class NoteProcessor
|
|
20
|
+
REDUNDANT_NOTE_REGEX =
|
|
21
|
+
%r{
|
|
22
|
+
^An? # Starts with "A" or "An"
|
|
23
|
+
\s.*?\sis\sa\stype\sof # Text followed by "is a type of"
|
|
24
|
+
(\sa|\san)? # Optional " a" or " an"
|
|
25
|
+
\s\{\{[^\}]*\}\} # Text in double curly braces
|
|
26
|
+
\s*?\.?$ # Optional whitespace and period at the end
|
|
27
|
+
}x
|
|
28
|
+
|
|
29
|
+
IMAGE_REF_SUBSTRING = "image::"
|
|
30
|
+
DOUBLE_ANGLE_PATTERN = /<<(.*?){1,999}>>/
|
|
31
|
+
SEE_TAIL_PATTERN = /\s+\(see(.*?){1,999}\)/
|
|
32
|
+
XREF_WITH_DISPLAY = /<<express:([\w.]+),([\w. ][\w. ]*)>>/
|
|
33
|
+
XREF_WITHOUT_DISPLAY = /<<express:([\w.]+)>>/
|
|
34
|
+
INLINE_COMMENT_PATTERN = /\n\/\/.*?\n/
|
|
35
|
+
BULLET_LIST_PATTERN = /^\s*[*\-+]\s+/
|
|
36
|
+
NUMBERED_LIST_PATTERN = /^\s*\d+\.\s+/
|
|
37
|
+
CONTINUATION_PLUS = /^\+\s*$/
|
|
38
|
+
CONTINUATION_DASHES = /^--\s*$/
|
|
39
|
+
INDENTED_PATTERN = /^\s{2,}/
|
|
40
|
+
SENTENCE_BREAK_NEWLINE = /\.\n/
|
|
41
|
+
SENTENCE_BREAK_SPACE = /\. /
|
|
42
|
+
|
|
43
|
+
attr_reader :remarks, :definitions, :xref_to_mention
|
|
44
|
+
|
|
45
|
+
def initialize(remarks, definitions:, xref_to_mention:)
|
|
46
|
+
@remarks = remarks
|
|
47
|
+
@definitions = definitions
|
|
48
|
+
@xref_to_mention = xref_to_mention
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.call(remarks, definitions:, xref_to_mention:)
|
|
52
|
+
new(remarks, definitions: definitions, xref_to_mention: xref_to_mention).call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call
|
|
56
|
+
notes = build_initial_notes
|
|
57
|
+
notes = only_keep_first_sentence(notes)
|
|
58
|
+
notes = remove_see_content(notes)
|
|
59
|
+
notes = remove_redundant_note(notes)
|
|
60
|
+
notes = remove_invalid_references(notes)
|
|
61
|
+
compare_with_definitions(notes, definitions)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_initial_notes
|
|
67
|
+
trimmed = trim_definition(remarks)
|
|
68
|
+
return [] unless trimmed && !trimmed.empty?
|
|
69
|
+
|
|
70
|
+
[Glossarist::V3::DetailedDefinition.new(content: trimmed)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def trim_definition(definition)
|
|
74
|
+
return nil if definition.nil? || definition.empty?
|
|
75
|
+
|
|
76
|
+
definition_str = array_to_paragraphs(definition)
|
|
77
|
+
return nil if definition_str.empty?
|
|
78
|
+
|
|
79
|
+
paragraphs = definition_str.split("\n\n")
|
|
80
|
+
combine_paragraphs(paragraphs)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def array_to_paragraphs(definition)
|
|
84
|
+
definition.is_a?(Array) ? definition.join("\n\n") : definition.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def combine_paragraphs(paragraphs)
|
|
88
|
+
combined = first_block(paragraphs)
|
|
89
|
+
combined = "#{combined}\n"
|
|
90
|
+
combined = combined.gsub(INLINE_COMMENT_PATTERN, "\n")
|
|
91
|
+
combined.strip!
|
|
92
|
+
convert_xref(combined)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def first_block(paragraphs)
|
|
96
|
+
first_paragraph = paragraphs.first
|
|
97
|
+
return apply_first_sentence_logic(first_paragraph) unless list_header?(paragraphs)
|
|
98
|
+
|
|
99
|
+
complete_list = extract_complete_list(paragraphs, 1)
|
|
100
|
+
"#{first_paragraph}\n\n#{complete_list}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def list_header?(paragraphs)
|
|
104
|
+
paragraphs.length > 1 &&
|
|
105
|
+
paragraphs.first.end_with?(":") &&
|
|
106
|
+
starts_with_list?(paragraphs[1])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_first_sentence_logic(paragraph)
|
|
110
|
+
new_content = paragraph
|
|
111
|
+
.split(SENTENCE_BREAK_NEWLINE).first.strip
|
|
112
|
+
.split(SENTENCE_BREAK_SPACE).first.strip
|
|
113
|
+
|
|
114
|
+
new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_complete_list(paragraphs, start_index)
|
|
118
|
+
return paragraphs[start_index] if start_index >= paragraphs.length
|
|
119
|
+
|
|
120
|
+
reduce_list_paragraphs(paragraphs, start_index)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reduce_list_paragraphs(paragraphs, start_index)
|
|
124
|
+
combined = paragraphs[start_index].dup
|
|
125
|
+
state = ContinuationState.new(combined.include?("--") && !combined.match?(/--.*--/m))
|
|
126
|
+
|
|
127
|
+
((start_index + 1)...paragraphs.length).each do |i|
|
|
128
|
+
break unless apply_paragraph_step?(combined, paragraphs[i], state)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
combined
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns true if iteration should continue, false to stop.
|
|
135
|
+
def apply_paragraph_step?(combined, para, state)
|
|
136
|
+
case classify_paragraph(para, state)
|
|
137
|
+
when :continuation_toggle
|
|
138
|
+
state.toggle!
|
|
139
|
+
append!(combined, para)
|
|
140
|
+
when :continuation
|
|
141
|
+
append!(combined, para)
|
|
142
|
+
when :list_member
|
|
143
|
+
append!(combined, para)
|
|
144
|
+
state.toggle! if opens_continuation_block?(para)
|
|
145
|
+
else
|
|
146
|
+
return false
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def append!(combined, para)
|
|
152
|
+
combined << "\n\n#{para}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def opens_continuation_block?(para)
|
|
156
|
+
para.include?("--") && !para.match?(/--.*--/m)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def classify_paragraph(para, state)
|
|
160
|
+
return :continuation_toggle if para.match?(CONTINUATION_DASHES) || para.end_with?("--")
|
|
161
|
+
return :continuation if state.in_block?
|
|
162
|
+
return :list_member if starts_with_list?(para) || list_continuation?(para)
|
|
163
|
+
|
|
164
|
+
:stop
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Mutable flag tracking whether we are inside a "-- ... --"
|
|
168
|
+
# continuation block while walking paragraphs.
|
|
169
|
+
class ContinuationState
|
|
170
|
+
def initialize(in_block)
|
|
171
|
+
@in_block = in_block
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def in_block?
|
|
175
|
+
@in_block
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def toggle!
|
|
179
|
+
@in_block = !@in_block
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
private_constant :ContinuationState
|
|
183
|
+
|
|
184
|
+
def list_continuation?(content)
|
|
185
|
+
return false if content.nil? || content.empty?
|
|
186
|
+
|
|
187
|
+
content.match?(CONTINUATION_PLUS) ||
|
|
188
|
+
content.match?(CONTINUATION_DASHES) ||
|
|
189
|
+
content.match?(INDENTED_PATTERN) ||
|
|
190
|
+
content.start_with?("which", "where", "that")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def starts_with_list?(content)
|
|
194
|
+
return false if content.nil? || content.empty?
|
|
195
|
+
|
|
196
|
+
content.match?(BULLET_LIST_PATTERN) || content.match?(NUMBERED_LIST_PATTERN)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def only_keep_first_sentence(notes)
|
|
200
|
+
notes.each do |note|
|
|
201
|
+
next unless note&.content
|
|
202
|
+
next if preserve_complete_structure?(note.content)
|
|
203
|
+
|
|
204
|
+
new_content = note.content
|
|
205
|
+
.split(SENTENCE_BREAK_NEWLINE).first.strip
|
|
206
|
+
.split(SENTENCE_BREAK_SPACE).first.strip
|
|
207
|
+
note.content = new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def preserve_complete_structure?(content)
|
|
212
|
+
return false if content.nil? || content.empty?
|
|
213
|
+
|
|
214
|
+
lines = content.split("\n")
|
|
215
|
+
return false unless first_line_is_header?(lines)
|
|
216
|
+
return false if first_line_has_period?(lines.first.strip)
|
|
217
|
+
|
|
218
|
+
remaining_content = lines[1..].join("\n")
|
|
219
|
+
starts_with_list?(remaining_content.strip)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def first_line_is_header?(lines)
|
|
223
|
+
lines.first&.strip&.end_with?(":") && lines.length > 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def first_line_has_period?(first_line)
|
|
227
|
+
first_line.count(".").positive?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def remove_see_content(notes)
|
|
231
|
+
notes.each do |note|
|
|
232
|
+
note.content = note.content.gsub(SEE_TAIL_PATTERN, "")
|
|
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_invalid_references(notes)
|
|
244
|
+
notes.reject do |note|
|
|
245
|
+
note.content.include?(IMAGE_REF_SUBSTRING) ||
|
|
246
|
+
note.content.match?(DOUBLE_ANGLE_PATTERN)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def compare_with_definitions(notes, definitions)
|
|
251
|
+
return [] if notes&.first&.content == definitions&.first&.content
|
|
252
|
+
|
|
253
|
+
notes
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def convert_xref(content)
|
|
257
|
+
content
|
|
258
|
+
.gsub(XREF_WITHOUT_DISPLAY) do |_match|
|
|
259
|
+
full_ref = Regexp.last_match(1)
|
|
260
|
+
display = full_ref.split(".").last
|
|
261
|
+
xref_to_mention.call(full_ref, display)
|
|
262
|
+
end
|
|
263
|
+
.gsub(XREF_WITH_DISPLAY) do |_match|
|
|
264
|
+
full_ref = Regexp.last_match(1)
|
|
265
|
+
display = Regexp.last_match(2)
|
|
266
|
+
xref_to_mention.call(full_ref, display)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
data/lib/suma/schema_comparer.rb
CHANGED
|
@@ -91,7 +91,7 @@ module Suma
|
|
|
91
91
|
existing_schema = Expressir::Changes::SchemaChange.from_file(output_path)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
converter = EengineConverter.new(
|
|
94
|
+
converter = EengineConverter.new(schema_name, File.read(xml_path))
|
|
95
95
|
change_schema = converter.convert(
|
|
96
96
|
version: options[:version],
|
|
97
97
|
existing_change_schema: existing_schema,
|
data/lib/suma/schema_exporter.rb
CHANGED
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Exports EXPRESS schemas to a directory, with optional ZIP packaging.
|
|
7
|
+
#
|
|
8
|
+
# Pure sink: the exporter accepts already-loaded +Suma::ExpressSchema+
|
|
9
|
+
# instances and writes their content to disk. Construction of those
|
|
10
|
+
# instances (with the right +output_path+ and +is_standalone_file+
|
|
11
|
+
# flags) is the caller's responsibility — the exporter does not
|
|
12
|
+
# reach across the seam to inspect manifest entries or classify
|
|
13
|
+
# schema types itself.
|
|
14
|
+
#
|
|
15
|
+
# This is a deep module: a small interface (one +export+ method, one
|
|
16
|
+
# option hash) backed by save_exp + zip packaging. The CLI and
|
|
17
|
+
# SchemaCollection adapters construct ExpressSchema instances; the
|
|
18
|
+
# exporter never inspects their shape.
|
|
8
19
|
class SchemaExporter
|
|
9
20
|
attr_reader :schemas, :output_path, :options
|
|
10
21
|
|
|
@@ -35,30 +46,7 @@ module Suma
|
|
|
35
46
|
|
|
36
47
|
def export_to_directory(schemas)
|
|
37
48
|
schemas.each do |schema|
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def export_single_schema(schema)
|
|
43
|
-
is_standalone = !schema.is_a?(Expressir::SchemaManifestEntry)
|
|
44
|
-
schema_output_path = determine_output_path(schema, is_standalone)
|
|
45
|
-
|
|
46
|
-
express_schema = ExpressSchema.new(
|
|
47
|
-
id: schema.id,
|
|
48
|
-
path: schema.path.to_s,
|
|
49
|
-
output_path: schema_output_path,
|
|
50
|
-
is_standalone_file: is_standalone,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
express_schema.save_exp(with_annotations: options[:annotations])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def determine_output_path(schema, is_standalone)
|
|
57
|
-
if is_standalone
|
|
58
|
-
output_path.to_s
|
|
59
|
-
else
|
|
60
|
-
category = SchemaCategory.for_schema(id: schema.id, path: schema.path)
|
|
61
|
-
output_path.join(category.directory).to_s
|
|
49
|
+
schema.save_exp(with_annotations: options[:annotations])
|
|
62
50
|
end
|
|
63
51
|
end
|
|
64
52
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "svg_conform"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module SvgQuality
|
|
7
|
+
# Deep module behind the SVG-quality seam: takes paths in, returns
|
|
8
|
+
# +Report+ / +BatchReport+ out. Owns validator construction and
|
|
9
|
+
# per-file result capture. Does not own file discovery, sorting,
|
|
10
|
+
# filtering, or presentation — those stay in the CLI adapter.
|
|
11
|
+
#
|
|
12
|
+
# The progress adapter is injected: pass any object responding to
|
|
13
|
+
# +#call(index, total, report)+. +NullProgress+ is the default
|
|
14
|
+
# no-op; the CLI passes a lambda that writes to +$stderr+.
|
|
15
|
+
class Scanner
|
|
16
|
+
DEFAULT_PROFILE = :metanorma
|
|
17
|
+
|
|
18
|
+
attr_reader :profile, :progress
|
|
19
|
+
|
|
20
|
+
def initialize(profile: DEFAULT_PROFILE,
|
|
21
|
+
progress: NullProgress.new)
|
|
22
|
+
@profile = profile
|
|
23
|
+
@progress = progress
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def scan(paths)
|
|
27
|
+
validator = build_validator
|
|
28
|
+
reports = paths.each_with_index.map do |path, index|
|
|
29
|
+
scan_one(validator, path, index, paths.size)
|
|
30
|
+
end
|
|
31
|
+
BatchReport.new(reports)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def scan_file(path)
|
|
35
|
+
Report.new(path.to_s, build_validator.validate_file(path.to_s,
|
|
36
|
+
profile: profile))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Default no-op progress adapter. Real progress reporters are
|
|
40
|
+
# passed by the caller; this satisfies the same interface so the
|
|
41
|
+
# scanner can be invoked from specs without forcing the
|
|
42
|
+
# dependency.
|
|
43
|
+
class NullProgress
|
|
44
|
+
def call(_index, _total, _report); end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_validator
|
|
50
|
+
SvgConform::Validator.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def scan_one(validator, path, index, total)
|
|
54
|
+
result = validator.validate_file(path.to_s, profile: profile)
|
|
55
|
+
report = Report.new(path.to_s, result)
|
|
56
|
+
progress.call(index, total, report)
|
|
57
|
+
report
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/suma/svg_quality.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Suma
|
|
|
6
6
|
module SvgQuality
|
|
7
7
|
autoload :Report, "suma/svg_quality/report"
|
|
8
8
|
autoload :BatchReport, "suma/svg_quality/batch_report"
|
|
9
|
+
autoload :Scanner, "suma/svg_quality/scanner"
|
|
9
10
|
|
|
10
11
|
module QualityTiers
|
|
11
12
|
CRITICAL = { name: :critical, min_errors: 200, emoji: "💥" }.freeze
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Term-extractor-specific classification of an EXPRESS schema.
|
|
5
|
+
#
|
|
6
|
+
# Bridges ExpressSchema::Type (the canonical classification shared
|
|
7
|
+
# across the codebase) and the Glossarist-specific labels
|
|
8
|
+
# TermExtractor emits: the domain string ("application module" /
|
|
9
|
+
# "resource") that goes into a concept's +domain+ field, and the
|
|
10
|
+
# entity-type URN term used in generated concept definitions.
|
|
11
|
+
#
|
|
12
|
+
# The mapping is data — a frozen Hash keyed by ExpressSchema::Type
|
|
13
|
+
# symbol — so adding a new schema type is a one-line addition to
|
|
14
|
+
# BY_TYPE (open/closed principle). The previous implementation
|
|
15
|
+
# switched on string keys in three separate places; this consolidates
|
|
16
|
+
# them into one source of truth.
|
|
17
|
+
class TermClassification
|
|
18
|
+
attr_reader :type, :domain_label, :entity_term, :entity_display
|
|
19
|
+
|
|
20
|
+
def initialize(type:, domain_label:, entity_term:, entity_display:)
|
|
21
|
+
@type = type
|
|
22
|
+
@domain_label = domain_label
|
|
23
|
+
@entity_term = entity_term
|
|
24
|
+
@entity_display = entity_display
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def domain_for(schema_id)
|
|
29
|
+
"#{domain_label}: #{schema_id}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
BY_TYPE = {
|
|
33
|
+
ExpressSchema::Type::RESOURCE => new(
|
|
34
|
+
type: ExpressSchema::Type::RESOURCE,
|
|
35
|
+
domain_label: "resource",
|
|
36
|
+
entity_term: "express-language.entity_data_type",
|
|
37
|
+
entity_display: "entity data type",
|
|
38
|
+
),
|
|
39
|
+
ExpressSchema::Type::MODULE_ARM => new(
|
|
40
|
+
type: ExpressSchema::Type::MODULE_ARM,
|
|
41
|
+
domain_label: "application module",
|
|
42
|
+
entity_term: "general.application_object",
|
|
43
|
+
entity_display: "application object",
|
|
44
|
+
),
|
|
45
|
+
ExpressSchema::Type::MODULE_MIM => new(
|
|
46
|
+
type: ExpressSchema::Type::MODULE_MIM,
|
|
47
|
+
domain_label: "application module",
|
|
48
|
+
entity_term: "express-language.entity_data_type",
|
|
49
|
+
entity_display: "entity data type",
|
|
50
|
+
),
|
|
51
|
+
ExpressSchema::Type::BUSINESS_OBJECT_MODEL => new(
|
|
52
|
+
type: ExpressSchema::Type::BUSINESS_OBJECT_MODEL,
|
|
53
|
+
domain_label: "resource",
|
|
54
|
+
entity_term: "express-language.entity_data_type",
|
|
55
|
+
entity_display: "entity data type",
|
|
56
|
+
),
|
|
57
|
+
ExpressSchema::Type::CORE_MODEL => new(
|
|
58
|
+
type: ExpressSchema::Type::CORE_MODEL,
|
|
59
|
+
domain_label: "resource",
|
|
60
|
+
entity_term: "express-language.entity_data_type",
|
|
61
|
+
entity_display: "entity data type",
|
|
62
|
+
),
|
|
63
|
+
ExpressSchema::Type::STANDALONE => new(
|
|
64
|
+
type: ExpressSchema::Type::STANDALONE,
|
|
65
|
+
domain_label: "resource",
|
|
66
|
+
entity_term: "express-language.entity_data_type",
|
|
67
|
+
entity_display: "entity data type",
|
|
68
|
+
),
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
def self.for_schema(id:, path:)
|
|
72
|
+
type = ExpressSchema::Type.classify(id: id, path: path)
|
|
73
|
+
BY_TYPE.fetch(type) do |t|
|
|
74
|
+
raise Error, "[suma] no term classification for type #{t.inspect}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|