suma 0.2.5 → 0.3.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/.github/workflows/rake.yml +3 -0
- data/.github/workflows/release.yml +5 -1
- data/.gitignore +10 -1
- data/.rubocop_todo.yml +237 -28
- data/CLAUDE.md +102 -0
- data/Gemfile +3 -1
- data/README.adoc +188 -1
- data/exe/suma +1 -1
- data/lib/suma/cli/build.rb +2 -8
- data/lib/suma/cli/check_svg_quality.rb +172 -0
- data/lib/suma/cli/compare.rb +6 -158
- data/lib/suma/cli/convert_jsdai.rb +0 -2
- data/lib/suma/cli/core.rb +119 -0
- data/lib/suma/cli/export.rb +1 -10
- data/lib/suma/cli/extract_terms.rb +10 -654
- data/lib/suma/cli/generate_register.rb +34 -0
- data/lib/suma/cli/generate_schemas.rb +8 -124
- data/lib/suma/cli/reformat.rb +0 -1
- data/lib/suma/cli/validate.rb +0 -2
- data/lib/suma/cli/validate_links.rb +14 -291
- data/lib/suma/cli.rb +12 -102
- data/lib/suma/collection_config.rb +0 -2
- data/lib/suma/collection_manifest.rb +7 -111
- data/lib/suma/eengine/wrapper.rb +0 -1
- data/lib/suma/eengine.rb +8 -0
- data/lib/suma/express_schema.rb +43 -31
- data/lib/suma/jsdai/figure.rb +0 -3
- data/lib/suma/jsdai/figure_xml.rb +12 -9
- data/lib/suma/jsdai.rb +5 -8
- data/lib/suma/link_validator.rb +211 -0
- data/lib/suma/manifest_traverser.rb +92 -0
- data/lib/suma/processor.rb +76 -105
- data/lib/suma/register_manifest_generator.rb +163 -0
- data/lib/suma/schema_category.rb +83 -0
- data/lib/suma/schema_collection.rb +28 -63
- data/lib/suma/schema_comparer.rb +117 -0
- data/lib/suma/schema_compiler.rb +86 -0
- data/lib/suma/schema_discovery.rb +75 -0
- data/lib/suma/schema_exporter.rb +7 -35
- data/lib/suma/schema_index.rb +53 -0
- data/lib/suma/schema_manifest_generator.rb +113 -0
- data/lib/suma/schema_naming.rb +111 -0
- data/lib/suma/schema_template/document.rb +141 -0
- data/lib/suma/schema_template/plain.rb +46 -0
- data/lib/suma/schema_template.rb +19 -0
- data/lib/suma/svg_quality/batch_report.rb +78 -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/formatters.rb +12 -0
- data/lib/suma/svg_quality/report.rb +52 -0
- data/lib/suma/svg_quality.rb +30 -0
- data/lib/suma/term_extractor.rb +466 -0
- data/lib/suma/urn.rb +61 -0
- data/lib/suma/utils.rb +10 -2
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +34 -5
- data/suma.gemspec +3 -2
- metadata +53 -9
- data/lib/suma/export_standalone_schema.rb +0 -14
- data/lib/suma/schema_attachment.rb +0 -130
- data/lib/suma/schema_document.rb +0 -132
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SchemaTemplate
|
|
5
|
+
# Emits an AsciiDoc body with cross-reference anchors for every
|
|
6
|
+
# schema element so other documents can deep-link into the compiled
|
|
7
|
+
# HTML. Only XML is produced — the anchors only resolve against the
|
|
8
|
+
# XML output, not the HTML rendering.
|
|
9
|
+
class Document
|
|
10
|
+
EXTENSIONS = "xml"
|
|
11
|
+
|
|
12
|
+
attr_reader :schema_id
|
|
13
|
+
|
|
14
|
+
def initialize(schema_id)
|
|
15
|
+
@schema_id = schema_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extensions
|
|
19
|
+
EXTENSIONS
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(path_to_schema_yaml)
|
|
23
|
+
<<~ADOC
|
|
24
|
+
= #{schema_id}
|
|
25
|
+
:lutaml-express-index: schemas; #{path_to_schema_yaml};
|
|
26
|
+
:bare: true
|
|
27
|
+
:mn-document-class: iso
|
|
28
|
+
:mn-output-extensions: #{extensions}
|
|
29
|
+
|
|
30
|
+
[lutaml_express_liquid,schemas,context]
|
|
31
|
+
----
|
|
32
|
+
{% for schema in context.schemas %}
|
|
33
|
+
|
|
34
|
+
[[#{schema_id}]]
|
|
35
|
+
[%unnumbered,type=express]
|
|
36
|
+
== #{schema_id} #{rendered_anchors}
|
|
37
|
+
|
|
38
|
+
[source%unnumbered]
|
|
39
|
+
--
|
|
40
|
+
{{ schema.formatted }}
|
|
41
|
+
--
|
|
42
|
+
{% endfor %}
|
|
43
|
+
----
|
|
44
|
+
|
|
45
|
+
ADOC
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The raw anchor block contains Liquid comments and newlines that
|
|
51
|
+
# are illegal on the section header line; strip them before
|
|
52
|
+
# interpolating.
|
|
53
|
+
def rendered_anchors
|
|
54
|
+
schema_anchors.gsub(%r{//[^\r\n]+}, "").gsub(/[\n\r]+/, "").gsub(
|
|
55
|
+
/^[\n\r]/, ""
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def schema_anchors
|
|
60
|
+
<<~HEREDOC
|
|
61
|
+
// _fund_cons.liquid
|
|
62
|
+
[[#{schema_id}_funds]]
|
|
63
|
+
|
|
64
|
+
// _constants.liquid
|
|
65
|
+
{% if schema.constants.size > 0 %}
|
|
66
|
+
#{bookmark('constants')}
|
|
67
|
+
{% for thing in schema.constants %}
|
|
68
|
+
#{bookmark('{{thing.id}}')}
|
|
69
|
+
{% endfor %}
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
// _types.liquid
|
|
73
|
+
{% if schema.types.size > 0 %}
|
|
74
|
+
#{bookmark('types')}
|
|
75
|
+
// _type.liquid
|
|
76
|
+
{% for thing in schema.types %}
|
|
77
|
+
#{bookmark('{{thing.id}}')}
|
|
78
|
+
{% if thing.items.size > 0 %}
|
|
79
|
+
// _type_items.liquid
|
|
80
|
+
#{bookmark('{{thing.id}}.items')}
|
|
81
|
+
{% for item in thing.items %}
|
|
82
|
+
#{bookmark('{{thing.id}}.items.{{item.id}}')}
|
|
83
|
+
{% endfor %}
|
|
84
|
+
{% endif %}
|
|
85
|
+
{% endfor %}
|
|
86
|
+
{% endif %}
|
|
87
|
+
|
|
88
|
+
// _entities.liquid
|
|
89
|
+
{% if schema.entities.size > 0 %}
|
|
90
|
+
#{bookmark('entities')}
|
|
91
|
+
{% for thing in schema.entities %}
|
|
92
|
+
// _entity.liquid
|
|
93
|
+
#{bookmark('{{thing.id}}')}
|
|
94
|
+
{% endfor %}
|
|
95
|
+
{% endif %}
|
|
96
|
+
|
|
97
|
+
// _subtype_constraints.liquid
|
|
98
|
+
{% if schema.subtype_constraints.size > 0 %}
|
|
99
|
+
#{bookmark('subtype_constraints')}
|
|
100
|
+
// _subtype_constraint.liquid
|
|
101
|
+
{% for thing in schema.subtype_constraints %}
|
|
102
|
+
#{bookmark('{{thing.id}}')}
|
|
103
|
+
{% endfor %}
|
|
104
|
+
{% endif %}
|
|
105
|
+
|
|
106
|
+
// _functions.liquid
|
|
107
|
+
{% if schema.functions.size > 0 %}
|
|
108
|
+
#{bookmark('functions')}
|
|
109
|
+
// _function.liquid
|
|
110
|
+
{% for thing in schema.functions %}
|
|
111
|
+
#{bookmark('{{thing.id}}')}
|
|
112
|
+
{% endfor %}
|
|
113
|
+
{% endif %}
|
|
114
|
+
|
|
115
|
+
// _procedures.liquid
|
|
116
|
+
{% if schema.procedures.size > 0 %}
|
|
117
|
+
#{bookmark('procedures')}
|
|
118
|
+
// _procedure.liquid
|
|
119
|
+
{% for thing in schema.procedures %}
|
|
120
|
+
#{bookmark('{{thing.id}}')}
|
|
121
|
+
{% endfor %}
|
|
122
|
+
{% endif %}
|
|
123
|
+
|
|
124
|
+
// _rules.liquid
|
|
125
|
+
{% if schema.rules.size > 0 %}
|
|
126
|
+
#{bookmark('rules')}
|
|
127
|
+
// _rule.liquid
|
|
128
|
+
{% for thing in schema.rules %}
|
|
129
|
+
#{bookmark('{{thing.id}}')}
|
|
130
|
+
{% endfor %}
|
|
131
|
+
{% endif %}
|
|
132
|
+
HEREDOC
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def bookmark(anchor)
|
|
136
|
+
mangled = anchor.gsub("}}", ' | replace: "\", "-", "-}}')
|
|
137
|
+
"[[#{schema_id}.#{mangled}]]"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SchemaTemplate
|
|
5
|
+
# Emits a plain AsciiDoc body for a single EXPRESS schema, producing
|
|
6
|
+
# both HTML and XML outputs via Metanorma.
|
|
7
|
+
class Plain
|
|
8
|
+
EXTENSIONS = "xml,html"
|
|
9
|
+
|
|
10
|
+
attr_reader :schema_id
|
|
11
|
+
|
|
12
|
+
def initialize(schema_id)
|
|
13
|
+
@schema_id = schema_id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def extensions
|
|
17
|
+
EXTENSIONS
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(path_to_schema_yaml)
|
|
21
|
+
<<~ADOC
|
|
22
|
+
= #{schema_id}
|
|
23
|
+
:lutaml-express-index: schemas; #{path_to_schema_yaml};
|
|
24
|
+
:bare: true
|
|
25
|
+
:mn-document-class: iso
|
|
26
|
+
:mn-output-extensions: #{extensions}
|
|
27
|
+
|
|
28
|
+
[lutaml_express_liquid,schemas,context]
|
|
29
|
+
----
|
|
30
|
+
{% for schema in context.schemas %}
|
|
31
|
+
|
|
32
|
+
[%unnumbered]
|
|
33
|
+
== #{schema_id}
|
|
34
|
+
|
|
35
|
+
[source%unnumbered]
|
|
36
|
+
--
|
|
37
|
+
{{ schema.formatted }}
|
|
38
|
+
--
|
|
39
|
+
{% endfor %}
|
|
40
|
+
----
|
|
41
|
+
|
|
42
|
+
ADOC
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Pure renderers for the AsciiDoc source fed to Metanorma when compiling
|
|
5
|
+
# an EXPRESS schema to HTML/XML.
|
|
6
|
+
#
|
|
7
|
+
# Each template knows how to produce the adoc body for one compilation
|
|
8
|
+
# flavour (plain HTML, or HTML with cross-reference anchors). Templates
|
|
9
|
+
# have no I/O and no knowledge of the underlying ExpressSchema — they
|
|
10
|
+
# only need the schema id, because the rendered adoc is consumed by
|
|
11
|
+
# Liquid inside Metanorma, which fetches the schema by id from the
|
|
12
|
+
# surrounding lutaml-express-index.
|
|
13
|
+
#
|
|
14
|
+
# Composition with the compiler lives in SchemaCompiler.
|
|
15
|
+
module SchemaTemplate
|
|
16
|
+
autoload :Plain, "suma/schema_template/plain"
|
|
17
|
+
autoload :Document, "suma/schema_template/document"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SvgQuality
|
|
5
|
+
# Batch report wrapping multiple SVG quality reports
|
|
6
|
+
class BatchReport
|
|
7
|
+
attr_reader :reports
|
|
8
|
+
|
|
9
|
+
def initialize(reports)
|
|
10
|
+
@reports = reports
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def total_files
|
|
14
|
+
@reports.size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def successful
|
|
18
|
+
@reports.count(&:valid?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failed
|
|
22
|
+
total_files - successful
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def avg_quality_score
|
|
26
|
+
return 0 if @reports.empty?
|
|
27
|
+
|
|
28
|
+
@reports.sum(&:quality_score).to_f / total_files
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def total_errors
|
|
32
|
+
@reports.sum(&:error_count)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def avg_error_count
|
|
36
|
+
return 0 if @reports.empty?
|
|
37
|
+
|
|
38
|
+
total_errors.to_f / total_files
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def quality_distribution
|
|
42
|
+
dist = Hash.new(0)
|
|
43
|
+
@reports.each do |r|
|
|
44
|
+
dist[r.quality_tier[:name].to_s] += 1
|
|
45
|
+
end
|
|
46
|
+
dist
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sort_by_quality
|
|
50
|
+
self.class.new(@reports.sort_by(&:quality_score))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort_by_errors
|
|
54
|
+
self.class.new(@reports.sort_by { |r| -r.error_count })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def limit(count)
|
|
58
|
+
return self if count.nil?
|
|
59
|
+
|
|
60
|
+
self.class.new(@reports.first(count))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def filter_by_min_errors(min)
|
|
64
|
+
return self if min.nil?
|
|
65
|
+
|
|
66
|
+
self.class.new(@reports.select { |r| r.error_count >= min })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_json(*_args)
|
|
70
|
+
JSON.pretty_generate(@reports.map(&:to_h))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_yaml
|
|
74
|
+
YAML.dump(@reports.map(&:to_h))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SvgQuality
|
|
5
|
+
module Formatters
|
|
6
|
+
# JSON output formatter
|
|
7
|
+
class JsonFormatter
|
|
8
|
+
def initialize(batch_report, output: nil)
|
|
9
|
+
@batch_report = batch_report
|
|
10
|
+
@output = output
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format
|
|
14
|
+
write_output(@batch_report.to_json)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def write_output(content)
|
|
20
|
+
if @output
|
|
21
|
+
File.write(@output, content)
|
|
22
|
+
"[suma] Results written to #{@output}"
|
|
23
|
+
else
|
|
24
|
+
content
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module SvgQuality
|
|
7
|
+
module Formatters
|
|
8
|
+
# Terminal output formatter with ASCII art and emojis
|
|
9
|
+
class TerminalFormatter
|
|
10
|
+
BORDER = "─"
|
|
11
|
+
BOX_WIDTH = 80
|
|
12
|
+
|
|
13
|
+
def initialize(batch_report, output: nil, sort: :quality)
|
|
14
|
+
@batch_report = batch_report
|
|
15
|
+
@output = output
|
|
16
|
+
@sort = sort.to_sym
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format
|
|
20
|
+
output_content = [
|
|
21
|
+
header,
|
|
22
|
+
"",
|
|
23
|
+
summary_section,
|
|
24
|
+
"",
|
|
25
|
+
distribution_section,
|
|
26
|
+
"",
|
|
27
|
+
files_by_tier_section,
|
|
28
|
+
"",
|
|
29
|
+
footer,
|
|
30
|
+
].join("\n")
|
|
31
|
+
|
|
32
|
+
write_output(output_content)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :batch_report
|
|
38
|
+
|
|
39
|
+
def header
|
|
40
|
+
sort_label = case @sort
|
|
41
|
+
when :errors then "error count (most first)"
|
|
42
|
+
else "quality score (lowest first)"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
"╔#{'═' * (BOX_WIDTH - 2)}╗\n" \
|
|
46
|
+
"║ 🔍 SVG Quality Report Sorted by #{sort_label.ljust(33)}║\n" \
|
|
47
|
+
"╚#{'═' * (BOX_WIDTH - 2)}╝"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def summary_section
|
|
51
|
+
lines = []
|
|
52
|
+
lines << " 📊 OVERVIEW"
|
|
53
|
+
lines << ""
|
|
54
|
+
lines << " ● Total Files : #{batch_report.total_files}"
|
|
55
|
+
lines << " ● Valid : #{batch_report.successful} ✅"
|
|
56
|
+
lines << " ● Invalid : #{batch_report.failed} ❌"
|
|
57
|
+
lines << " ● Avg Score : #{batch_report.avg_quality_score.round(1)}/100"
|
|
58
|
+
lines << " ● Total Errors : #{batch_report.total_errors}"
|
|
59
|
+
lines << " ● Avg Errors : #{batch_report.avg_error_count.round(1)}/file"
|
|
60
|
+
lines << ""
|
|
61
|
+
|
|
62
|
+
if (worst = batch_report.reports.first)
|
|
63
|
+
tier = worst.quality_tier
|
|
64
|
+
lines << " 🚨 WORST OFFENDER"
|
|
65
|
+
lines << ""
|
|
66
|
+
lines << " #{tier[:emoji]} #{shorten_path(worst.file_path)}"
|
|
67
|
+
lines << " Score: #{worst.quality_score}/100 | Errors: #{worst.error_count} | #{tier[:name].to_s.upcase}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
lines.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def distribution_section
|
|
74
|
+
lines = []
|
|
75
|
+
lines << " 📈 QUALITY DISTRIBUTION"
|
|
76
|
+
lines << ""
|
|
77
|
+
|
|
78
|
+
total = batch_report.total_files
|
|
79
|
+
dist = batch_report.quality_distribution
|
|
80
|
+
|
|
81
|
+
QualityTiers::ALL.each do |tier|
|
|
82
|
+
count = dist[tier[:name].to_s].to_i
|
|
83
|
+
pct = total.positive? ? (count.to_f / total * 100) : 0
|
|
84
|
+
bar_len = (count.to_f / total * 40).round
|
|
85
|
+
bar = bar_len.positive? ? "█" * bar_len : ""
|
|
86
|
+
empty = "░" * (40 - bar_len)
|
|
87
|
+
|
|
88
|
+
lines << " #{tier[:emoji]} #{tier[:name].to_s.upcase.ljust(9)} #{bar}#{empty} #{count.to_s.rjust(4)} (#{sprintf(
|
|
89
|
+
'%.1f', pct
|
|
90
|
+
)}%)"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
lines.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def files_by_tier_section
|
|
97
|
+
lines = []
|
|
98
|
+
|
|
99
|
+
if @sort == :errors
|
|
100
|
+
# When sorting by errors, show flat list (worst offenders first)
|
|
101
|
+
lines << ""
|
|
102
|
+
lines << " 📋 ALL FILES (sorted by error count, worst first)"
|
|
103
|
+
lines << ""
|
|
104
|
+
|
|
105
|
+
batch_report.reports.each do |report|
|
|
106
|
+
lines << format_file_line(report)
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
# When sorting by quality, group by tier - iterate CRITICAL first (worst first)
|
|
110
|
+
reports_by_tier = batch_report.reports.group_by do |r|
|
|
111
|
+
r.quality_tier[:name]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
QualityTiers::ALL.each do |tier|
|
|
115
|
+
tier_reports = reports_by_tier[tier[:name]]
|
|
116
|
+
next unless tier_reports&.any?
|
|
117
|
+
|
|
118
|
+
lines << ""
|
|
119
|
+
lines << " #{tier[:emoji]} #{tier[:name].to_s.upcase} QUALITY (#{tier_reports.size} files)"
|
|
120
|
+
lines << ""
|
|
121
|
+
|
|
122
|
+
tier_reports.each do |report|
|
|
123
|
+
lines << format_file_line(report)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
lines.join("\n")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def format_file_line(report)
|
|
132
|
+
path = shorten_path(report.file_path)
|
|
133
|
+
score = report.quality_score.to_i.to_s.rjust(3)
|
|
134
|
+
errors = report.error_count.to_s.rjust(5)
|
|
135
|
+
valid_str = report.valid? ? "✓" : "✗"
|
|
136
|
+
|
|
137
|
+
" #{valid_str} #{score}/100 #{errors} errors #{path}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def shorten_path(path)
|
|
141
|
+
p = Pathname.new(path)
|
|
142
|
+
if p.absolute?
|
|
143
|
+
begin
|
|
144
|
+
p.relative_path_from(Pathname.pwd)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
p
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
p
|
|
150
|
+
end.to_s
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def footer
|
|
154
|
+
BORDER * BOX_WIDTH
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def write_output(content)
|
|
158
|
+
if @output
|
|
159
|
+
File.write(@output, content)
|
|
160
|
+
"[suma] Results written to #{@output}"
|
|
161
|
+
else
|
|
162
|
+
content
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module SvgQuality
|
|
7
|
+
module Formatters
|
|
8
|
+
# YAML output formatter
|
|
9
|
+
class YamlFormatter
|
|
10
|
+
def initialize(batch_report, output: nil)
|
|
11
|
+
@batch_report = batch_report
|
|
12
|
+
@output = output
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def format
|
|
16
|
+
write_output(@batch_report.to_yaml)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def write_output(content)
|
|
22
|
+
if @output
|
|
23
|
+
File.write(@output, content)
|
|
24
|
+
"[suma] Results written to #{@output}"
|
|
25
|
+
else
|
|
26
|
+
content
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
module SvgQuality
|
|
5
|
+
module Formatters
|
|
6
|
+
autoload :TerminalFormatter,
|
|
7
|
+
"suma/svg_quality/formatters/terminal_formatter"
|
|
8
|
+
autoload :JsonFormatter, "suma/svg_quality/formatters/json_formatter"
|
|
9
|
+
autoload :YamlFormatter, "suma/svg_quality/formatters/yaml_formatter"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -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,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "svg_conform"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module SvgQuality
|
|
7
|
+
autoload :Report, "suma/svg_quality/report"
|
|
8
|
+
autoload :BatchReport, "suma/svg_quality/batch_report"
|
|
9
|
+
|
|
10
|
+
module QualityTiers
|
|
11
|
+
CRITICAL = { name: :critical, min_errors: 200, emoji: "💥" }.freeze
|
|
12
|
+
HIGH = { name: :high, min_errors: 100, emoji: "🔴" }.freeze
|
|
13
|
+
MEDIUM = { name: :medium, min_errors: 50, emoji: "⚠️" }.freeze
|
|
14
|
+
LOW = { name: :low, min_errors: 20, emoji: "🔶" }.freeze
|
|
15
|
+
MINOR = { name: :minor, min_errors: 0, emoji: "✅" }.freeze
|
|
16
|
+
|
|
17
|
+
ALL = [CRITICAL, HIGH, MEDIUM, LOW, MINOR].freeze
|
|
18
|
+
|
|
19
|
+
def self.for_error_count(count)
|
|
20
|
+
ALL.find { |tier| count >= tier[:min_errors] } || MINOR
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def validator(profile: :svg_1_2_rfc)
|
|
26
|
+
SvgConform::Validator.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|