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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +3 -0
  3. data/.github/workflows/release.yml +5 -1
  4. data/.rubocop_todo.yml +78 -26
  5. data/CLAUDE.md +76 -0
  6. data/Gemfile +3 -1
  7. data/README.adoc +131 -0
  8. data/lib/suma/cli/build.rb +2 -3
  9. data/lib/suma/cli/check_svg_quality.rb +178 -0
  10. data/lib/suma/cli/compare.rb +7 -158
  11. data/lib/suma/cli/export.rb +1 -7
  12. data/lib/suma/cli/extract_terms.rb +7 -648
  13. data/lib/suma/cli/generate_schemas.rb +9 -123
  14. data/lib/suma/cli/validate_links.rb +15 -290
  15. data/lib/suma/cli.rb +39 -0
  16. data/lib/suma/collection_manifest.rb +3 -4
  17. data/lib/suma/express_schema.rb +43 -30
  18. data/lib/suma/jsdai/figure_xml.rb +12 -9
  19. data/lib/suma/jsdai.rb +0 -6
  20. data/lib/suma/link_validator.rb +203 -0
  21. data/lib/suma/processor.rb +75 -101
  22. data/lib/suma/schema_attachment.rb +2 -29
  23. data/lib/suma/schema_collection.rb +1 -32
  24. data/lib/suma/schema_comparer.rb +116 -0
  25. data/lib/suma/schema_document.rb +0 -14
  26. data/lib/suma/schema_exporter.rb +16 -28
  27. data/lib/suma/schema_index.rb +53 -0
  28. data/lib/suma/schema_manifest_generator.rb +105 -0
  29. data/lib/suma/svg_quality/batch_report.rb +80 -0
  30. data/lib/suma/svg_quality/formatters/json_formatter.rb +30 -0
  31. data/lib/suma/svg_quality/formatters/terminal_formatter.rb +168 -0
  32. data/lib/suma/svg_quality/formatters/yaml_formatter.rb +32 -0
  33. data/lib/suma/svg_quality/report.rb +52 -0
  34. data/lib/suma/svg_quality.rb +28 -0
  35. data/lib/suma/term_extractor.rb +393 -0
  36. data/lib/suma/utils.rb +10 -2
  37. data/lib/suma/version.rb +1 -1
  38. data/lib/suma.rb +3 -2
  39. data/suma.gemspec +3 -2
  40. metadata +33 -7
  41. data/lib/suma/export_standalone_schema.rb +0 -14
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+ require_relative "eengine/wrapper"
6
+ require_relative "eengine_converter"
7
+
8
+ module Suma
9
+ class SchemaComparer
10
+ attr_reader :trial_schema, :reference_schema, :options
11
+
12
+ def initialize(trial_schema, reference_schema, options = {})
13
+ @trial_schema = trial_schema
14
+ @reference_schema = reference_schema
15
+ @options = options
16
+ end
17
+
18
+ def compare
19
+ validate_inputs
20
+
21
+ trial_stepmod = options[:trial_stepmod] || detect_repo_root(trial_schema)
22
+ reference_stepmod = options[:reference_stepmod] || detect_repo_root(reference_schema)
23
+
24
+ out_dir = Dir.mktmpdir("eengine-compare-")
25
+
26
+ result = Eengine::Wrapper.compare(
27
+ trial_schema,
28
+ reference_schema,
29
+ mode: options[:mode] || "resource",
30
+ trial_stepmod: trial_stepmod,
31
+ reference_stepmod: reference_stepmod,
32
+ out_dir: out_dir,
33
+ )
34
+
35
+ unless result[:has_changes]
36
+ FileUtils.rm_rf(out_dir) if File.directory?(out_dir)
37
+ return nil
38
+ end
39
+
40
+ raise Suma::CompilationError, "XML output not found" unless result[:xml_path]
41
+
42
+ convert_to_change_yaml(result[:xml_path], out_dir)
43
+ ensure
44
+ FileUtils.rm_rf(out_dir) if out_dir && File.directory?(out_dir)
45
+ end
46
+
47
+ private
48
+
49
+ def validate_inputs
50
+ unless File.exist?(trial_schema)
51
+ raise Suma::SchemaNotFoundError,
52
+ "Trial schema not found: #{trial_schema}"
53
+ end
54
+
55
+ unless File.exist?(reference_schema)
56
+ raise Suma::SchemaNotFoundError,
57
+ "Reference schema not found: #{reference_schema}"
58
+ end
59
+
60
+ unless Eengine::Wrapper.available?
61
+ raise Suma::EengineNotAvailableError,
62
+ "eengine not found in PATH. Install from:\n " \
63
+ "macOS: https://github.com/expresslang/homebrew-eengine\n " \
64
+ "Linux: https://github.com/expresslang/eengine-releases"
65
+ end
66
+ end
67
+
68
+ def detect_repo_root(schema_path)
69
+ current = File.expand_path(File.dirname(schema_path))
70
+
71
+ loop do
72
+ return current if File.directory?(File.join(current, ".git"))
73
+
74
+ parent = File.dirname(current)
75
+ break if parent == current
76
+
77
+ current = parent
78
+ end
79
+
80
+ File.dirname(schema_path)
81
+ end
82
+
83
+ def convert_to_change_yaml(xml_path, _out_dir)
84
+ schema_name = extract_schema_name(trial_schema)
85
+ output_path = determine_output_path
86
+
87
+ existing_schema = nil
88
+ if File.exist?(output_path)
89
+ require "expressir/changes"
90
+ existing_schema = Expressir::Changes::SchemaChange.from_file(output_path)
91
+ end
92
+
93
+ converter = EengineConverter.new(xml_path, schema_name)
94
+ change_schema = converter.convert(
95
+ version: options[:version],
96
+ existing_change_schema: existing_schema,
97
+ )
98
+
99
+ change_schema.to_file(output_path)
100
+ output_path
101
+ end
102
+
103
+ def extract_schema_name(path)
104
+ basename = File.basename(path, ".exp")
105
+ basename.sub(/_\d+$/, "")
106
+ end
107
+
108
+ def determine_output_path
109
+ options[:output] || begin
110
+ base = extract_schema_name(trial_schema)
111
+ dir = File.dirname(trial_schema)
112
+ File.join(dir, "#{base}.changes.yaml")
113
+ end
114
+ end
115
+ end
116
+ end
@@ -85,24 +85,10 @@ module Suma
85
85
  HEREDOC
86
86
  end
87
87
 
88
- # ////
89
- # TODO:
90
- # % render "templates/entities", schema: schema, schema_id: schema.id, things: schema.entities, thing_prefix: root_thing_prefix, depth: 2 %
91
- #
92
- # % render "templates/subtype_constraints", schema_id: schema.id, things: schema.subtype_constraints, thing_prefix: root_thing_prefix, depth: 2 %
93
- #
94
- # % render "templates/functions", schema_id: schema.id, things: schema.functions, thing_prefix: root_thing_prefix, depth: 2 %
95
- #
96
- # % render "templates/procedures", schema_id: schema.id, things: schema.procedures, thing_prefix: root_thing_prefix, depth: 2 %
97
- #
98
- # % render "templates/rules", schema_id: schema.id, things: schema.rules, thing_prefix: root_thing_prefix, depth: 2 %
99
- # ////
100
-
101
88
  def output_extensions
102
89
  "xml"
103
90
  end
104
91
 
105
- # #.gsub(/[\n\r]{2,}/, '')
106
92
  def to_adoc(path_to_schema_yaml)
107
93
  <<~HEREDOC
108
94
  = #{@schema.id}
@@ -2,13 +2,21 @@
2
2
 
3
3
  require_relative "express_schema"
4
4
  require_relative "utils"
5
- require_relative "export_standalone_schema"
6
5
  require "fileutils"
7
6
 
8
7
  module Suma
9
8
  # SchemaExporter exports EXPRESS schemas from a manifest
10
9
  # with configurable options for annotations and ZIP packaging
11
10
  class SchemaExporter
11
+ CATEGORY_MAP = {
12
+ ExpressSchema::Type::RESOURCE => "resources",
13
+ ExpressSchema::Type::MODULE_ARM => "modules",
14
+ ExpressSchema::Type::MODULE_MIM => "modules",
15
+ ExpressSchema::Type::BUSINESS_OBJECT_MODEL => "business_object_models",
16
+ ExpressSchema::Type::CORE_MODEL => "core_model",
17
+ ExpressSchema::Type::STANDALONE => ".",
18
+ }.freeze
19
+
12
20
  attr_reader :schemas, :output_path, :options
13
21
 
14
22
  def initialize(schemas:, output_path:, options: {})
@@ -43,52 +51,32 @@ module Suma
43
51
  end
44
52
 
45
53
  def export_single_schema(schema)
46
- # Check if this is a standalone EXPRESS file
47
- # (not from a manifest structure)
48
- is_standalone_file = schema.is_a?(ExportStandaloneSchema)
49
- schema_output_path = determine_output_path(schema, is_standalone_file)
54
+ is_standalone = !schema.is_a?(Expressir::SchemaManifestEntry)
55
+ schema_output_path = determine_output_path(schema, is_standalone)
50
56
 
51
57
  express_schema = ExpressSchema.new(
52
58
  id: schema.id,
53
59
  path: schema.path.to_s,
54
60
  output_path: schema_output_path,
55
- is_standalone_file: is_standalone_file,
61
+ is_standalone_file: is_standalone,
56
62
  )
57
63
 
58
64
  express_schema.save_exp(with_annotations: options[:annotations])
59
65
  end
60
66
 
61
- def determine_output_path(schema, is_standalone_file)
62
- if is_standalone_file
63
- # For standalone files, output directly to the root
67
+ def determine_output_path(schema, is_standalone)
68
+ if is_standalone
64
69
  output_path.to_s
65
70
  else
66
- # For manifest schemas, preserve directory structure
67
71
  category = categorize_schema(schema)
68
72
  output_path.join(category).to_s
69
73
  end
70
74
  end
71
75
 
72
- # rubocop:disable Metrics/MethodLength
73
76
  def categorize_schema(schema)
74
- path = schema.path.to_s
75
-
76
- # Check if this is from a manifest structure or a standalone EXPRESS file
77
- case path
78
- when %r{/resources/}
79
- "resources"
80
- when %r{/modules/}
81
- "modules"
82
- when %r{/business_object_models/}
83
- "business_object_models"
84
- when %r{/core_model/}
85
- "core_model"
86
- else
87
- # standalone EXPRESS file not from a manifest structure
88
- "standalone"
89
- end
77
+ type = ExpressSchema::Type.classify(id: schema.id, path: schema.path)
78
+ CATEGORY_MAP.fetch(type, "standalone")
90
79
  end
91
- # rubocop:enable Metrics/MethodLength
92
80
 
93
81
  # rubocop:disable Metrics/MethodLength
94
82
  def create_zip_archive
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "expressir"
4
+
5
+ module Suma
6
+ # Pre-built index for O(1) schema and element lookup.
7
+ # Build once from a parsed repo, then query by name.
8
+ class SchemaIndex
9
+ def initialize(repo)
10
+ @schemas_by_name = {}
11
+ @elements_by_schema = {}
12
+
13
+ repo.schemas.each do |schema|
14
+ key = schema.id.downcase
15
+ @schemas_by_name[key] = schema
16
+ @elements_by_schema[key] = build_element_index(schema)
17
+ end
18
+ end
19
+
20
+ def find_schema(name)
21
+ @schemas_by_name[name.downcase]
22
+ end
23
+
24
+ def find_element(schema_name, element_name)
25
+ elements = @elements_by_schema[schema_name.downcase]
26
+ elements&.[](element_name.downcase)
27
+ end
28
+
29
+ private
30
+
31
+ def build_element_index(schema)
32
+ index = {}
33
+
34
+ element_collections(schema).each do |collection|
35
+ collection&.each { |e| index[e.id.downcase] = e }
36
+ end
37
+
38
+ index
39
+ end
40
+
41
+ def element_collections(schema)
42
+ [
43
+ schema.entities,
44
+ schema.types,
45
+ schema.constants,
46
+ schema.functions,
47
+ schema.rules,
48
+ schema.procedures,
49
+ schema.subtype_constraints,
50
+ ]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+ require_relative "utils"
6
+
7
+ module Suma
8
+ class SchemaManifestGenerator
9
+ YAML_FILE_EXTENSIONS = [".yaml", ".yml"].freeze
10
+
11
+ def initialize(metanorma_manifest_file, schema_manifest_file, exclude_paths: nil)
12
+ @metanorma_manifest_file = File.expand_path(metanorma_manifest_file)
13
+ @schema_manifest_file = schema_manifest_file
14
+ @exclude_paths = exclude_paths
15
+ end
16
+
17
+ def generate
18
+ validate_inputs
19
+ metanorma_data = load_yaml(@metanorma_manifest_file)
20
+ collection_files = metanorma_data["metanorma"]["source"]["files"]
21
+ manifest_files = load_manifest_files(collection_files)
22
+ all_schemas = load_project_schemas(manifest_files)
23
+ all_schemas["schemas"] = all_schemas["schemas"].sort.to_h
24
+ write_output(all_schemas)
25
+ end
26
+
27
+ private
28
+
29
+ def validate_inputs
30
+ raise Errno::ENOENT, "Specified file `#{@metanorma_manifest_file}` not found." unless File.exist?(@metanorma_manifest_file)
31
+
32
+ raise ArgumentError, "Specified path `#{@metanorma_manifest_file}` is not a file." unless File.file?(@metanorma_manifest_file)
33
+
34
+ [@metanorma_manifest_file, @schema_manifest_file].each do |file|
35
+ unless YAML_FILE_EXTENSIONS.include?(File.extname(file))
36
+ raise ArgumentError, "Specified file `#{file}` is not a YAML file."
37
+ end
38
+ end
39
+ end
40
+
41
+ def load_yaml(file_path)
42
+ YAML.safe_load(File.read(file_path, encoding: "UTF-8"), aliases: true)
43
+ end
44
+
45
+ def load_manifest_files(collection_files)
46
+ collection_files.map do |c|
47
+ collection_data = load_yaml(c)
48
+ collection_data["manifest"]["docref"].map { |docref| docref["file"] }
49
+ end.flatten
50
+ end
51
+
52
+ def load_project_schemas(manifest_files)
53
+ all_schemas = { "schemas" => {} }
54
+
55
+ manifest_files.each do |file|
56
+ schemas_file_path = File.expand_path(file.gsub("collection.yml", "schemas.yaml"))
57
+
58
+ unless File.exist?(schemas_file_path)
59
+ Utils.log "Schemas file not found: #{schemas_file_path}"
60
+ next
61
+ end
62
+
63
+ schemas_data = load_yaml(schemas_file_path)
64
+
65
+ if schemas_data["schemas"]
66
+ schemas_data["schemas"] = fix_path(schemas_data, schemas_file_path)
67
+ all_schemas["schemas"].merge!(schemas_data["schemas"])
68
+ end
69
+
70
+ if @exclude_paths
71
+ all_schemas["schemas"].delete_if do |_key, value|
72
+ value["path"].match?(
73
+ Regexp.new(@exclude_paths.gsub("*", "(.*){1,999}")),
74
+ )
75
+ end
76
+ end
77
+ end
78
+
79
+ all_schemas
80
+ end
81
+
82
+ def fix_path(schemas_data, schemas_file_path)
83
+ schema_manifest_path = File.expand_path(@schema_manifest_file, Dir.pwd)
84
+
85
+ schemas_data["schemas"].each do |key, value|
86
+ path_in_schema = File.expand_path(value["path"], File.dirname(schemas_file_path))
87
+
88
+ fixed_path = Pathname.new(path_in_schema).relative_path_from(
89
+ Pathname.new(File.dirname(schema_manifest_path)),
90
+ )
91
+
92
+ { key => value.merge!("path" => fixed_path.to_s) }
93
+ end
94
+
95
+ schemas_data["schemas"]
96
+ end
97
+
98
+ def write_output(all_schemas)
99
+ output_path = File.expand_path(@schema_manifest_file)
100
+ Utils.log "Writing the Schemas YAML file to #{output_path}..."
101
+ File.write(output_path, all_schemas.to_yaml)
102
+ Utils.log "Writing the Schemas YAML file to #{output_path}...Done"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "report"
4
+
5
+ module Suma
6
+ module SvgQuality
7
+ # Batch report wrapping multiple SVG quality reports
8
+ class BatchReport
9
+ attr_reader :reports
10
+
11
+ def initialize(reports)
12
+ @reports = reports
13
+ end
14
+
15
+ def total_files
16
+ @reports.size
17
+ end
18
+
19
+ def successful
20
+ @reports.count(&:valid?)
21
+ end
22
+
23
+ def failed
24
+ total_files - successful
25
+ end
26
+
27
+ def avg_quality_score
28
+ return 0 if @reports.empty?
29
+
30
+ @reports.sum(&:quality_score).to_f / total_files
31
+ end
32
+
33
+ def total_errors
34
+ @reports.sum(&:error_count)
35
+ end
36
+
37
+ def avg_error_count
38
+ return 0 if @reports.empty?
39
+
40
+ total_errors.to_f / total_files
41
+ end
42
+
43
+ def quality_distribution
44
+ dist = Hash.new(0)
45
+ @reports.each do |r|
46
+ dist[r.quality_tier[:name].to_s] += 1
47
+ end
48
+ dist
49
+ end
50
+
51
+ def sort_by_quality
52
+ self.class.new(@reports.sort_by(&:quality_score))
53
+ end
54
+
55
+ def sort_by_errors
56
+ self.class.new(@reports.sort_by { |r| -r.error_count })
57
+ end
58
+
59
+ def limit(count)
60
+ return self if count.nil?
61
+
62
+ self.class.new(@reports.first(count))
63
+ end
64
+
65
+ def filter_by_min_errors(min)
66
+ return self if min.nil?
67
+
68
+ self.class.new(@reports.select { |r| r.error_count >= min })
69
+ end
70
+
71
+ def to_json(*_args)
72
+ JSON.pretty_generate(@reports.map(&:to_h))
73
+ end
74
+
75
+ def to_yaml
76
+ YAML.dump(@reports.map(&:to_h))
77
+ end
78
+ end
79
+ end
80
+ 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