xsdvi 1.0.0 → 1.0.1
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/README.adoc +364 -6
- data/lib/xsdvi/cli.rb +162 -10
- data/lib/xsdvi/comparison/dual_generator.rb +190 -0
- data/lib/xsdvi/comparison/html_generator.rb +181 -0
- data/lib/xsdvi/comparison/java_manager.rb +124 -0
- data/lib/xsdvi/comparison/metadata_extractor.rb +75 -0
- data/lib/xsdvi/svg/generator.rb +1 -0
- data/lib/xsdvi/svg/symbol.rb +39 -30
- data/lib/xsdvi/svg/symbols/attribute.rb +1 -1
- data/lib/xsdvi/svg/symbols/element.rb +1 -1
- data/lib/xsdvi/version.rb +1 -1
- data/lib/xsdvi/xsd_handler.rb +459 -24
- data/lib/xsdvi.rb +6 -0
- data/resources/comparison/template.html +234 -0
- data/resources/svg/defined_symbols.svg +9 -9
- data/resources/svg/menu_buttons.svg +6 -6
- data/resources/svg/script.js +264 -264
- data/resources/svg/style.css +28 -28
- data/resources/svg/style.html +2 -2
- metadata +7 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
module Xsdvi
|
|
7
|
+
module Comparison
|
|
8
|
+
# Orchestrates dual generation with Java and Ruby XsdVi
|
|
9
|
+
class DualGenerator
|
|
10
|
+
attr_reader :xsd_file, :output_dir
|
|
11
|
+
|
|
12
|
+
def initialize(xsd_file, options = {})
|
|
13
|
+
@xsd_file = xsd_file
|
|
14
|
+
@root = options[:root_node_name]
|
|
15
|
+
@output_dir = options[:output_path] || default_output_dir
|
|
16
|
+
@skip_java = options[:skip_java]
|
|
17
|
+
@skip_ruby = options[:skip_ruby]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Generate comparison
|
|
21
|
+
# @return [Hash] Generation results with paths and metadata
|
|
22
|
+
def generate
|
|
23
|
+
validate_inputs
|
|
24
|
+
setup_directories
|
|
25
|
+
|
|
26
|
+
# Generate Java output
|
|
27
|
+
java_start = Time.now
|
|
28
|
+
generate_java unless @skip_java
|
|
29
|
+
java_time = Time.now - java_start
|
|
30
|
+
|
|
31
|
+
# Generate Ruby output
|
|
32
|
+
ruby_start = Time.now
|
|
33
|
+
generate_ruby unless @skip_ruby
|
|
34
|
+
ruby_time = Time.now - ruby_start
|
|
35
|
+
|
|
36
|
+
# Extract metadata
|
|
37
|
+
java_metadata = extract_metadata(java_dir)
|
|
38
|
+
ruby_metadata = extract_metadata(ruby_dir)
|
|
39
|
+
|
|
40
|
+
# Add timing info
|
|
41
|
+
java_metadata[:generation_time] = java_time.round(2) unless @skip_java
|
|
42
|
+
ruby_metadata[:generation_time] = ruby_time.round(2) unless @skip_ruby
|
|
43
|
+
|
|
44
|
+
# Generate HTML comparison
|
|
45
|
+
html_file = generate_html(java_metadata, ruby_metadata)
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
output_dir: @output_dir,
|
|
49
|
+
html_file: html_file,
|
|
50
|
+
java: java_metadata,
|
|
51
|
+
ruby: ruby_metadata,
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Validate inputs
|
|
58
|
+
def validate_inputs
|
|
59
|
+
raise "XSD file not found: #{@xsd_file}" unless File.exist?(@xsd_file)
|
|
60
|
+
raise "Must generate at least one implementation" if @skip_java && @skip_ruby
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Setup output directories
|
|
64
|
+
def setup_directories
|
|
65
|
+
FileUtils.mkdir_p(java_dir)
|
|
66
|
+
FileUtils.mkdir_p(ruby_dir)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get Java output directory
|
|
70
|
+
# @return [String] Java directory path
|
|
71
|
+
def java_dir
|
|
72
|
+
File.join(@output_dir, "java")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get Ruby output directory
|
|
76
|
+
# @return [String] Ruby directory path
|
|
77
|
+
def ruby_dir
|
|
78
|
+
File.join(@output_dir, "ruby")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Generate Java XsdVi output
|
|
82
|
+
def generate_java
|
|
83
|
+
puts "Generating Java XsdVi output..."
|
|
84
|
+
manager = JavaManager.new
|
|
85
|
+
|
|
86
|
+
options = {}
|
|
87
|
+
options[:root] = @root if @root
|
|
88
|
+
|
|
89
|
+
manager.generate(@xsd_file, java_dir, options)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate Ruby XsdVi output
|
|
93
|
+
def generate_ruby
|
|
94
|
+
puts "Generating Ruby XsdVi output..."
|
|
95
|
+
|
|
96
|
+
# Set up the generation pipeline (same as CLI)
|
|
97
|
+
builder = Tree::Builder.new
|
|
98
|
+
xsd_handler = XsdHandler.new(builder)
|
|
99
|
+
writer_helper = Utils::Writer.new
|
|
100
|
+
svg_generator = SVG::Generator.new(writer_helper)
|
|
101
|
+
|
|
102
|
+
# Configure handler
|
|
103
|
+
xsd_handler.root_node_name = @root
|
|
104
|
+
# one_node_only should only be true when generating all elements separately
|
|
105
|
+
xsd_handler.one_node_only = (@root == "all")
|
|
106
|
+
|
|
107
|
+
# Configure generator
|
|
108
|
+
svg_generator.hide_menu_buttons = (@root == "all")
|
|
109
|
+
svg_generator.embody_style = true
|
|
110
|
+
|
|
111
|
+
# Parse XSD
|
|
112
|
+
xsd_handler.process_file(@xsd_file)
|
|
113
|
+
|
|
114
|
+
# Generate output file(s)
|
|
115
|
+
if @root == "all"
|
|
116
|
+
generate_ruby_all_elements(xsd_handler, svg_generator, builder,
|
|
117
|
+
writer_helper)
|
|
118
|
+
else
|
|
119
|
+
generate_ruby_single(svg_generator, builder, writer_helper)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Generate Ruby output for all elements
|
|
124
|
+
def generate_ruby_all_elements(xsd_handler, svg_generator, builder,
|
|
125
|
+
writer_helper)
|
|
126
|
+
doc = Nokogiri::XML(File.read(@xsd_file))
|
|
127
|
+
element_names = xsd_handler.get_elements_names(doc)
|
|
128
|
+
|
|
129
|
+
element_names.each do |elem_name|
|
|
130
|
+
# Reset for each element
|
|
131
|
+
builder = Tree::Builder.new
|
|
132
|
+
handler = XsdHandler.new(builder)
|
|
133
|
+
handler.root_node_name = elem_name
|
|
134
|
+
handler.one_node_only = true
|
|
135
|
+
handler.set_schema_namespace(doc, elem_name)
|
|
136
|
+
handler.process_file(@xsd_file)
|
|
137
|
+
|
|
138
|
+
output_file = File.join(ruby_dir, "#{elem_name}.svg")
|
|
139
|
+
writer_helper.new_writer(output_file)
|
|
140
|
+
svg_generator.draw(builder.root) if builder.root
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Generate Ruby output for single element
|
|
145
|
+
def generate_ruby_single(svg_generator, builder, writer_helper)
|
|
146
|
+
filename = if @root
|
|
147
|
+
"#{@root}.svg"
|
|
148
|
+
else
|
|
149
|
+
"#{File.basename(@xsd_file, '.xsd')}.svg"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
output_file = File.join(ruby_dir, filename)
|
|
153
|
+
writer_helper.new_writer(output_file)
|
|
154
|
+
svg_generator.draw(builder.root) if builder.root
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Extract metadata from output directory
|
|
158
|
+
# @param dir [String] Directory path
|
|
159
|
+
# @return [Hash] Extracted metadata
|
|
160
|
+
def extract_metadata(dir)
|
|
161
|
+
extractor = MetadataExtractor.new
|
|
162
|
+
extractor.extract(dir)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Generate HTML comparison page
|
|
166
|
+
# @param java_meta [Hash] Java metadata
|
|
167
|
+
# @param ruby_meta [Hash] Ruby metadata
|
|
168
|
+
# @return [String] Path to HTML file
|
|
169
|
+
def generate_html(java_meta, ruby_meta)
|
|
170
|
+
puts "Generating comparison HTML..."
|
|
171
|
+
|
|
172
|
+
generator = HtmlGenerator.new
|
|
173
|
+
generator.generate(
|
|
174
|
+
@output_dir,
|
|
175
|
+
java_meta,
|
|
176
|
+
ruby_meta,
|
|
177
|
+
schema_name: File.basename(@xsd_file, ".xsd"),
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Generate default output directory path
|
|
182
|
+
# @return [String] Default directory path
|
|
183
|
+
def default_output_dir
|
|
184
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
|
185
|
+
schema_name = File.basename(@xsd_file, ".xsd")
|
|
186
|
+
File.join("comparisons", "#{schema_name}-#{timestamp}")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Xsdvi
|
|
6
|
+
module Comparison
|
|
7
|
+
# Generates HTML comparison page
|
|
8
|
+
class HtmlGenerator
|
|
9
|
+
TEMPLATE_PATH = File.join(__dir__,
|
|
10
|
+
"../../../resources/comparison/template.html")
|
|
11
|
+
|
|
12
|
+
# Generate HTML comparison page
|
|
13
|
+
# @param output_dir [String] Output directory path
|
|
14
|
+
# @param java_metadata [Hash] Java generation metadata
|
|
15
|
+
# @param ruby_metadata [Hash] Ruby generation metadata
|
|
16
|
+
# @param options [Hash] Generation options
|
|
17
|
+
# @option options [String] :schema_name Schema name for display
|
|
18
|
+
# @return [String] Path to generated HTML file
|
|
19
|
+
def generate(output_dir, java_metadata, ruby_metadata, options = {})
|
|
20
|
+
template = File.read(TEMPLATE_PATH)
|
|
21
|
+
|
|
22
|
+
html = template
|
|
23
|
+
.gsub("{{SCHEMA_NAME}}", options[:schema_name] || "Schema")
|
|
24
|
+
.gsub("{{JAVA_METADATA}}", format_metadata_json(java_metadata))
|
|
25
|
+
.gsub("{{RUBY_METADATA}}", format_metadata_json(ruby_metadata))
|
|
26
|
+
.gsub("{{STATS_TABLE}}", generate_stats_table(java_metadata,
|
|
27
|
+
ruby_metadata))
|
|
28
|
+
.gsub("{{ELEMENT_SELECTOR}}", generate_element_selector(
|
|
29
|
+
java_metadata, ruby_metadata
|
|
30
|
+
))
|
|
31
|
+
.gsub("{{IS_MULTI_FILE}}", (java_metadata[:file_count] > 1).to_s)
|
|
32
|
+
|
|
33
|
+
output_file = File.join(output_dir, "comparison.html")
|
|
34
|
+
File.write(output_file, html)
|
|
35
|
+
|
|
36
|
+
output_file
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Format metadata as JSON
|
|
42
|
+
# @param metadata [Hash] Metadata hash
|
|
43
|
+
# @return [String] Pretty-printed JSON
|
|
44
|
+
def format_metadata_json(metadata)
|
|
45
|
+
JSON.pretty_generate(metadata)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate statistics comparison table HTML
|
|
49
|
+
# @param java_meta [Hash] Java metadata
|
|
50
|
+
# @param ruby_meta [Hash] Ruby metadata
|
|
51
|
+
# @return [String] HTML table
|
|
52
|
+
def generate_stats_table(java_meta, ruby_meta)
|
|
53
|
+
rows = []
|
|
54
|
+
|
|
55
|
+
# File counts
|
|
56
|
+
rows << table_row(
|
|
57
|
+
"Files",
|
|
58
|
+
java_meta[:file_count],
|
|
59
|
+
ruby_meta[:file_count],
|
|
60
|
+
java_meta[:file_count] == ruby_meta[:file_count],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Total size
|
|
64
|
+
rows << table_row(
|
|
65
|
+
"Total Size",
|
|
66
|
+
"#{java_meta[:total_size_kb]} KB",
|
|
67
|
+
"#{ruby_meta[:total_size_kb]} KB",
|
|
68
|
+
(java_meta[:total_size_kb] - ruby_meta[:total_size_kb]).abs < 1,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Generation time (if available)
|
|
72
|
+
if java_meta[:generation_time] && ruby_meta[:generation_time]
|
|
73
|
+
rows << table_row(
|
|
74
|
+
"Generation Time",
|
|
75
|
+
"#{java_meta[:generation_time]}s",
|
|
76
|
+
"#{ruby_meta[:generation_time]}s",
|
|
77
|
+
nil,
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Symbol counts (sum across all files)
|
|
82
|
+
symbol_types = [
|
|
83
|
+
["Total Symbols", :total_symbols],
|
|
84
|
+
["Elements", :elements],
|
|
85
|
+
["Optional Elements", :optional_elements],
|
|
86
|
+
["Attributes", :attributes],
|
|
87
|
+
["Sequences", :sequences],
|
|
88
|
+
["Choices", :choices],
|
|
89
|
+
["All Compositors", :all_compositors],
|
|
90
|
+
["Keys", :keys],
|
|
91
|
+
["Key References", :keyrefs],
|
|
92
|
+
["Unique Constraints", :uniques],
|
|
93
|
+
["Loops", :loops],
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
symbol_types.each do |label, key|
|
|
97
|
+
java_total = sum_symbol_count(java_meta[:files], key)
|
|
98
|
+
ruby_total = sum_symbol_count(ruby_meta[:files], key)
|
|
99
|
+
|
|
100
|
+
next if java_total.zero? && ruby_total.zero?
|
|
101
|
+
|
|
102
|
+
rows << table_row(label, java_total, ruby_total,
|
|
103
|
+
java_total == ruby_total)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
<<~HTML
|
|
107
|
+
<table>
|
|
108
|
+
<thead>
|
|
109
|
+
<tr>
|
|
110
|
+
<th>Metric</th>
|
|
111
|
+
<th>Java XsdVi</th>
|
|
112
|
+
<th>Ruby XsdVi</th>
|
|
113
|
+
<th>Match</th>
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
#{rows.join("\n ")}
|
|
118
|
+
</tbody>
|
|
119
|
+
</table>
|
|
120
|
+
HTML
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Generate a table row
|
|
124
|
+
# @param label [String] Row label
|
|
125
|
+
# @param java_val [Object] Java value
|
|
126
|
+
# @param ruby_val [Object] Ruby value
|
|
127
|
+
# @param match [Boolean, nil] Whether values match (nil for N/A)
|
|
128
|
+
# @return [String] HTML table row
|
|
129
|
+
def table_row(label, java_val, ruby_val, match)
|
|
130
|
+
match_cell = if match.nil?
|
|
131
|
+
"<td>—</td>"
|
|
132
|
+
elsif match
|
|
133
|
+
"<td class='match'>✓</td>"
|
|
134
|
+
else
|
|
135
|
+
"<td class='mismatch'>✗</td>"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
<<~HTML.strip
|
|
139
|
+
<tr>
|
|
140
|
+
<td><strong>#{label}</strong></td>
|
|
141
|
+
<td>#{java_val}</td>
|
|
142
|
+
<td>#{ruby_val}</td>
|
|
143
|
+
#{match_cell}
|
|
144
|
+
</tr>
|
|
145
|
+
HTML
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Sum symbol counts across files
|
|
149
|
+
# @param files [Array<Hash>] File metadata array
|
|
150
|
+
# @param key [Symbol] Symbol type key
|
|
151
|
+
# @return [Integer] Total count
|
|
152
|
+
def sum_symbol_count(files, key)
|
|
153
|
+
return 0 unless files
|
|
154
|
+
|
|
155
|
+
files.sum { |f| f[key] || 0 }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Generate element selector dropdown
|
|
159
|
+
# @param java_meta [Hash] Java metadata
|
|
160
|
+
# @param ruby_meta [Hash] Ruby metadata
|
|
161
|
+
# @return [String] HTML select element or empty string
|
|
162
|
+
def generate_element_selector(java_meta, _ruby_meta)
|
|
163
|
+
return "" if java_meta[:file_count] <= 1
|
|
164
|
+
|
|
165
|
+
options = java_meta[:files].each_with_index.map do |file, index|
|
|
166
|
+
name = File.basename(file[:name], ".svg")
|
|
167
|
+
"<option value='#{index}'>#{name}</option>"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
<<~HTML
|
|
171
|
+
<div class="element-selector">
|
|
172
|
+
<label for="element-select">Select Element:</label>
|
|
173
|
+
<select id="element-select" onchange="loadFile(this.value)">
|
|
174
|
+
#{options.join("\n ")}
|
|
175
|
+
</select>
|
|
176
|
+
</div>
|
|
177
|
+
HTML
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open-uri"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Xsdvi
|
|
7
|
+
module Comparison
|
|
8
|
+
# Manages downloading, caching, and executing Java XsdVi
|
|
9
|
+
class JavaManager
|
|
10
|
+
JAR_URL = "https://github.com/metanorma/xsdvi/releases/download/v1.3/xsdvi-1.3.jar"
|
|
11
|
+
CACHE_DIR = File.expand_path("~/.xsdvi")
|
|
12
|
+
JAR_FILENAME = "xsdvi-1.3.jar"
|
|
13
|
+
JAR_PATH = File.join(CACHE_DIR, JAR_FILENAME)
|
|
14
|
+
EXPECTED_SIZE = 2_500_000 # ~2.5MB minimum
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@jar_path = JAR_PATH
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Ensure JAR is available, download if needed
|
|
21
|
+
# @return [Boolean] true if JAR is ready
|
|
22
|
+
def ensure_jar_available
|
|
23
|
+
return true if jar_valid?
|
|
24
|
+
|
|
25
|
+
puts "Downloading Java XsdVi v1.3..."
|
|
26
|
+
download_jar
|
|
27
|
+
verify_jar
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Generate SVG using Java XsdVi
|
|
32
|
+
# @param xsd_file [String] Path to XSD file
|
|
33
|
+
# @param output_dir [String] Output directory path
|
|
34
|
+
# @param options [Hash] Generation options
|
|
35
|
+
# @option options [String] :root Root element name
|
|
36
|
+
# @option options [Boolean] :all Generate all elements
|
|
37
|
+
# @return [Boolean] true if successful
|
|
38
|
+
def generate(xsd_file, output_dir, options = {})
|
|
39
|
+
ensure_java_available
|
|
40
|
+
ensure_jar_available
|
|
41
|
+
|
|
42
|
+
cmd = build_java_command(xsd_file, output_dir, options)
|
|
43
|
+
puts "Executing Java XsdVi..."
|
|
44
|
+
|
|
45
|
+
success = system(cmd)
|
|
46
|
+
raise "Java XsdVi execution failed" unless success
|
|
47
|
+
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Check if cached JAR is valid
|
|
54
|
+
# @return [Boolean] true if JAR exists and has valid size
|
|
55
|
+
def jar_valid?
|
|
56
|
+
File.exist?(@jar_path) && File.size(@jar_path) > EXPECTED_SIZE
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Download JAR from GitHub releases
|
|
60
|
+
def download_jar
|
|
61
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
URI.open(JAR_URL, "rb") do |remote|
|
|
65
|
+
File.binwrite(@jar_path, remote.read)
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
raise "Failed to download Java XsdVi: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verify downloaded JAR integrity
|
|
73
|
+
def verify_jar
|
|
74
|
+
unless File.size(@jar_path) > EXPECTED_SIZE
|
|
75
|
+
File.delete(@jar_path)
|
|
76
|
+
raise "Downloaded JAR is invalid (size: #{File.size(@jar_path)} bytes)"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "✓ Java XsdVi cached at #{@jar_path}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Build Java command line
|
|
83
|
+
# @param xsd_file [String] Path to XSD file
|
|
84
|
+
# @param output_dir [String] Output directory path
|
|
85
|
+
# @param options [Hash] Generation options
|
|
86
|
+
# @return [String] Complete command string
|
|
87
|
+
def build_java_command(xsd_file, output_dir, options)
|
|
88
|
+
cmd_parts = ["java", "-jar", @jar_path]
|
|
89
|
+
|
|
90
|
+
# Input file is positional (no --in flag)
|
|
91
|
+
cmd_parts << File.expand_path(xsd_file)
|
|
92
|
+
|
|
93
|
+
# Root node name (including "all" for generating all elements)
|
|
94
|
+
if options[:root]
|
|
95
|
+
cmd_parts << "-rootNodeName" << options[:root]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One node only flag (required when generating all elements separately)
|
|
99
|
+
cmd_parts << "-oneNodeOnly" if options[:root] == "all"
|
|
100
|
+
|
|
101
|
+
# Output path
|
|
102
|
+
cmd_parts << "-outputPath" << File.expand_path(output_dir)
|
|
103
|
+
|
|
104
|
+
cmd_parts.join(" ")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if Java is available on system
|
|
108
|
+
# @raise [RuntimeError] if Java is not installed
|
|
109
|
+
def ensure_java_available
|
|
110
|
+
return if system("java -version", out: File::NULL, err: File::NULL)
|
|
111
|
+
|
|
112
|
+
raise <<~ERROR
|
|
113
|
+
Error: Java is required but not installed.
|
|
114
|
+
|
|
115
|
+
Please install Java from: https://java.com
|
|
116
|
+
Or use your system package manager:
|
|
117
|
+
- macOS: brew install openjdk
|
|
118
|
+
- Ubuntu/Debian: sudo apt-get install default-jdk
|
|
119
|
+
- Fedora: sudo dnf install java-latest-openjdk
|
|
120
|
+
ERROR
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Xsdvi
|
|
6
|
+
module Comparison
|
|
7
|
+
# Extracts metadata from SVG files for comparison
|
|
8
|
+
class MetadataExtractor
|
|
9
|
+
# Extract metadata from all SVG files in a directory
|
|
10
|
+
# @param svg_dir [String] Directory containing SVG files
|
|
11
|
+
# @return [Hash] Metadata including file count, sizes, and symbol counts
|
|
12
|
+
def extract(svg_dir)
|
|
13
|
+
files = Dir.glob(File.join(svg_dir, "*.svg"))
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
file_count: files.length,
|
|
17
|
+
total_size: files.sum { |f| File.size(f) },
|
|
18
|
+
total_size_kb: (files.sum { |f| File.size(f) } / 1024.0).round(1),
|
|
19
|
+
files: files.map { |f| analyze_file(f) },
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Analyze a single SVG file
|
|
26
|
+
# @param svg_file [String] Path to SVG file
|
|
27
|
+
# @return [Hash] File metadata and symbol counts
|
|
28
|
+
def analyze_file(svg_file)
|
|
29
|
+
content = File.read(svg_file)
|
|
30
|
+
doc = Nokogiri::XML(content)
|
|
31
|
+
# Remove namespaces to simplify XPath queries
|
|
32
|
+
doc.remove_namespaces!
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
name: File.basename(svg_file),
|
|
36
|
+
size: File.size(svg_file),
|
|
37
|
+
size_kb: (File.size(svg_file) / 1024.0).round(1),
|
|
38
|
+
elements: count_by_class(doc, "boxelement"),
|
|
39
|
+
optional_elements: count_by_class(doc, "boxelementoptional"),
|
|
40
|
+
attributes: count_by_class(doc, "boxattribute1", "boxattribute2"),
|
|
41
|
+
sequences: count_by_class(doc, "boxsequence"),
|
|
42
|
+
choices: count_by_class(doc, "boxchoice"),
|
|
43
|
+
all_compositors: count_by_class(doc, "boxall"),
|
|
44
|
+
compositors: count_by_class(doc, "boxcompositor"),
|
|
45
|
+
any: count_by_class(doc, "boxany"),
|
|
46
|
+
any_attribute: count_by_class(doc, "boxanyAttribute"),
|
|
47
|
+
keys: count_by_class(doc, "boxkey"),
|
|
48
|
+
keyrefs: count_by_class(doc, "boxkeyref"),
|
|
49
|
+
uniques: count_by_class(doc, "boxunique"),
|
|
50
|
+
selectors: count_by_class(doc, "boxselector"),
|
|
51
|
+
fields: count_by_class(doc, "boxfield"),
|
|
52
|
+
loops: count_by_class(doc, "boxloop"),
|
|
53
|
+
schemas: count_by_class(doc, "boxschema"),
|
|
54
|
+
total_symbols: count_all_boxes(doc),
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Count rectangles with specific CSS classes
|
|
59
|
+
# @param doc [Nokogiri::XML::Document] Parsed SVG document
|
|
60
|
+
# @param classes [Array<String>] CSS class names to match
|
|
61
|
+
# @return [Integer] Count of matching rectangles
|
|
62
|
+
def count_by_class(doc, *classes)
|
|
63
|
+
xpath = classes.map { |c| "@class='#{c}'" }.join(" or ")
|
|
64
|
+
doc.xpath("//rect[#{xpath}]").count
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Count all box elements (symbols)
|
|
68
|
+
# @param doc [Nokogiri::XML::Document] Parsed SVG document
|
|
69
|
+
# @return [Integer] Total count of box elements
|
|
70
|
+
def count_all_boxes(doc)
|
|
71
|
+
doc.xpath("//rect[starts-with(@class, 'box')]").count
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/xsdvi/svg/generator.rb
CHANGED
data/lib/xsdvi/svg/symbol.rb
CHANGED
|
@@ -155,10 +155,11 @@ module Xsdvi
|
|
|
155
155
|
wrapped_string = word_utils_wrap(desc_string, wrap_length)
|
|
156
156
|
wrapped_lines = wrapped_string.split("\n")
|
|
157
157
|
strings_with_breaks.concat(wrapped_lines)
|
|
158
|
+
# Match Java bug: accumulate TOTAL size, not just new lines
|
|
159
|
+
@additional_height += y_shift * strings_with_breaks.size
|
|
158
160
|
end
|
|
159
161
|
|
|
160
162
|
@description_string_array = strings_with_breaks
|
|
161
|
-
@additional_height = y_shift * strings_with_breaks.size
|
|
162
163
|
|
|
163
164
|
prev_y = Symbol.prev_y_position || 0
|
|
164
165
|
curr_y = y_position || 0
|
|
@@ -187,47 +188,55 @@ module Xsdvi
|
|
|
187
188
|
|
|
188
189
|
# Apache Commons WordUtils.wrap(str, wrapLength, newLineStr, wrapLongWords)
|
|
189
190
|
# Wraps text at wrapLength, inserting newLineStr, optionally breaking long words
|
|
191
|
+
# Java WordUtils processes multi-line input by handling each line separately
|
|
190
192
|
def word_utils_wrap(input, wrap_length)
|
|
191
193
|
return input if input.nil? || wrap_length < 1
|
|
192
194
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
# Split by existing newlines first (like Java WordUtils does)
|
|
196
|
+
input_lines = input.split("\n", -1) # -1 to preserve trailing empty strings
|
|
197
|
+
result_lines = []
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# Handle existing newline in input
|
|
201
|
-
space_idx = input.index("\n", offset)
|
|
202
|
-
if space_idx && space_idx < wrap_length + offset
|
|
203
|
-
# There's a newline before wrap point
|
|
204
|
-
result << input[offset...space_idx]
|
|
205
|
-
offset = space_idx + 1
|
|
199
|
+
input_lines.each do |line|
|
|
200
|
+
# Handle empty lines
|
|
201
|
+
if line.empty?
|
|
202
|
+
result_lines << ""
|
|
206
203
|
next
|
|
207
204
|
end
|
|
208
205
|
|
|
209
|
-
#
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
# Skip wrapping if line is strictly shorter than wrap_length
|
|
207
|
+
# Apache Commons only skips if length < wrap_length, not <=
|
|
208
|
+
if line.length < wrap_length
|
|
209
|
+
result_lines << line
|
|
210
|
+
next
|
|
214
211
|
end
|
|
215
212
|
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
213
|
+
# Wrap this line
|
|
214
|
+
offset = 0
|
|
215
|
+
line_length = line.length
|
|
216
|
+
|
|
217
|
+
while offset < line_length
|
|
218
|
+
# Rest of line fits?
|
|
219
|
+
if line_length - offset <= wrap_length
|
|
220
|
+
result_lines << line[offset..]
|
|
221
|
+
break
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Find last space before wrap point
|
|
225
|
+
space_idx = line.rindex(" ", offset + wrap_length)
|
|
226
|
+
|
|
227
|
+
if space_idx && space_idx >= offset
|
|
228
|
+
# Found space to break at
|
|
229
|
+
result_lines << line[offset...space_idx]
|
|
230
|
+
offset = space_idx + 1
|
|
231
|
+
else
|
|
232
|
+
# No space found - break at wrap_length (wrapLongWords=true)
|
|
233
|
+
result_lines << line[offset...(offset + wrap_length)]
|
|
234
|
+
offset += wrap_length
|
|
235
|
+
end
|
|
227
236
|
end
|
|
228
237
|
end
|
|
229
238
|
|
|
230
|
-
|
|
239
|
+
result_lines.join("\n")
|
|
231
240
|
end
|
|
232
241
|
end
|
|
233
242
|
end
|