suma 0.1.20 → 0.1.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 930231a5ca9c90a49c8e52b4a0881e2f93fd87508506872c02e693b9c0b8b25e
4
- data.tar.gz: 13202f05c7673f950e8d4de87a7088a044db5e0b393cf768889c3c0483baf174
3
+ metadata.gz: 414cf7549060d821671a08771418a9f3c163ba23af62ef980eb4b12ccd0f5ad4
4
+ data.tar.gz: 0b30377a2b1db558bf62c765f136c5c505ba905b387a5d76e249fc48fce8c237
5
5
  SHA512:
6
- metadata.gz: 63c387978764d7bf16fb2b8ea129038dbbf771a360415ddf09254ab14dcf33cdae44166fbdb60439e52026925c7755e4b631985ac0b41d9b2cc43311e3e2374c
7
- data.tar.gz: 8632ad3a285fa3a970805c7491812f07ed9501268f6cf3ce12fcb2be22b48d30a494e47bfbb40de1f4fe3f4c0e4822f5c2e3978637605f3afac7b2eb28edb8b9
6
+ metadata.gz: 7e899f31a08df29536d7abe1e8d2fc29a0c20038e1bb7dcedda35a7b9fa5d5041046e963d543401b4eaa100fa9d1aff19218b4e1c72457fb2a081a34211072f5
7
+ data.tar.gz: c764fc1985311dafb09074a498cfc30694427e95c2f29d4d243872fca5ddfa0ab6df3d323d785254b3ef69848cf3114c8b0b44b483ae09b97a1e293b05f82356
data/.rubocop_todo.yml CHANGED
@@ -1,26 +1,24 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-07-18 05:24:45 UTC using RuboCop version 1.78.0.
3
+ # on 2025-10-04 11:33:46 UTC using RuboCop version 1.81.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
9
  # Offense count: 3
10
- # Configuration parameters: Severity, Include.
11
- # Include: **/*.gemspec
10
+ # Configuration parameters: Severity.
12
11
  Gemspec/DuplicatedAssignment:
13
12
  Exclude:
14
13
  - 'suma.gemspec'
15
14
 
16
15
  # Offense count: 1
17
- # Configuration parameters: Severity, Include.
18
- # Include: **/*.gemspec
16
+ # Configuration parameters: Severity.
19
17
  Gemspec/RequiredRubyVersion:
20
18
  Exclude:
21
19
  - 'suma.gemspec'
22
20
 
23
- # Offense count: 28
21
+ # Offense count: 67
24
22
  # This cop supports safe autocorrection (--autocorrect).
25
23
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
26
24
  # URISchemes: http, https
@@ -29,6 +27,8 @@ Layout/LineLength:
29
27
  - 'lib/suma/cli.rb'
30
28
  - 'lib/suma/cli/build.rb'
31
29
  - 'lib/suma/cli/validate.rb'
30
+ - 'lib/suma/jsdai/figure.rb'
31
+ - 'lib/suma/jsdai/figure_image.rb'
32
32
  - 'lib/suma/processor.rb'
33
33
  - 'lib/suma/schema_attachment.rb'
34
34
  - 'lib/suma/schema_collection.rb'
@@ -36,27 +36,44 @@ Layout/LineLength:
36
36
  - 'lib/suma/thor_ext.rb'
37
37
  - 'spec/suma/cli/extract_terms_spec.rb'
38
38
  - 'spec/suma/cli/validate_ascii_spec.rb'
39
+ - 'spec/suma/jsdai/figure_spec.rb'
39
40
  - 'suma.gemspec'
40
41
 
42
+ # Offense count: 2
43
+ # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
44
+ Lint/DuplicateBranch:
45
+ Exclude:
46
+ - 'lib/suma/jsdai/figure.rb'
47
+
41
48
  # Offense count: 1
42
49
  Lint/DuplicateMethods:
43
50
  Exclude:
44
51
  - 'lib/suma/express_schema.rb'
45
52
 
46
- # Offense count: 4
53
+ # Offense count: 9
47
54
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
48
55
  Metrics/AbcSize:
49
56
  Exclude:
57
+ - 'lib/suma/jsdai/figure.rb'
58
+ - 'lib/suma/jsdai/figure_image.rb'
50
59
  - 'lib/suma/schema_attachment.rb'
51
60
  - 'lib/suma/schema_document.rb'
52
61
  - 'lib/suma/thor_ext.rb'
53
62
 
54
- # Offense count: 1
63
+ # Offense count: 3
55
64
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
56
65
  Metrics/CyclomaticComplexity:
57
66
  Exclude:
67
+ - 'lib/suma/jsdai/figure.rb'
68
+ - 'lib/suma/jsdai/figure_image.rb'
58
69
  - 'lib/suma/thor_ext.rb'
59
70
 
71
+ # Offense count: 1
72
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
73
+ Metrics/PerceivedComplexity:
74
+ Exclude:
75
+ - 'lib/suma/jsdai/figure_image.rb'
76
+
60
77
  # Offense count: 4
61
78
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
62
79
  # SupportedStyles: snake_case, normalcase, non_integer
@@ -71,11 +88,11 @@ Performance/CollectionLiteralInLoop:
71
88
  Exclude:
72
89
  - 'spec/suma/cli_spec.rb'
73
90
 
74
- # Offense count: 9
91
+ # Offense count: 24
75
92
  # Configuration parameters: CountAsOne.
76
93
  RSpec/ExampleLength:
77
94
  Max: 44
78
95
 
79
- # Offense count: 6
96
+ # Offense count: 21
80
97
  RSpec/MultipleExpectations:
81
98
  Max: 12
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in suma.gemspec
6
6
  gemspec
7
7
 
8
+ gem "nokogiri"
8
9
  gem "rake"
9
10
  gem "rspec"
10
11
  gem "rubocop"
data/README.adoc CHANGED
@@ -36,13 +36,14 @@ $ gem install suma
36
36
  ----
37
37
  # Defaults to `suma help`
38
38
  $ suma
39
- Commands:
40
- suma build METANORMA_SITE_MANIFEST # Build collection specified in site manifest (`metanorma*.yml`)
41
- suma reformat EXPRESS_FILE_PATH # Reformat EXPRESS files
42
- suma validate SUBCOMMAND ...ARGS # Validate express documents
43
- suma generate_schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE # Generate schemas manifest file from Metanorma manifest YAML file
44
- suma extract_terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH # Extract terms from schema manifest file
45
- suma help [COMMAND] # Describe available commands or one specific command
39
+ Commands:
40
+ suma build METANORMA_SITE_MANIFEST # Build collection specified in site manifest (`metanorma*.yml`)
41
+ suma convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR # Convert JSDAI XML and image files to SVG and EXP files
42
+ suma extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH # Extract terms from SCHEMA_MANIFEST_FILE into Glossarist v2 format
43
+ suma generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE # Generate EXPRESS schema manifest file from Metanorma site manifest
44
+ suma help [COMMAND] # Describe available commands or one specific command
45
+ suma reformat EXPRESS_FILE_PATH # Reformat EXPRESS files
46
+ suma validate SUBCOMMAND ...ARGS # Validate express documents
46
47
  ----
47
48
 
48
49
  === Build command
@@ -291,13 +292,13 @@ Replacement: AsciiMath: xx
291
292
 
292
293
  === Generate schemas command
293
294
 
294
- The `suma generate_schemas` command generates an EXPRESS schema manifest file
295
+ The `suma generate-schemas` command generates an EXPRESS schema manifest file
295
296
  containing all schemas of documents referenced in the Metanorma manifest file,
296
297
  recursively.
297
298
 
298
299
  [source,sh]
299
300
  ----
300
- $ suma generate_schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE [options]
301
+ $ suma generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE [options]
301
302
  ----
302
303
 
303
304
  Parameters:
@@ -314,14 +315,14 @@ Options:
314
315
  .To generate schemas manifest file from Metanorma manifest file
315
316
  [source,sh]
316
317
  ----
317
- $ bundle exec suma generate_schemas metanorma-smrl-all.yml schemas-smrl-all.yml
318
+ $ bundle exec suma generate-schemas metanorma-smrl-all.yml schemas-smrl-all.yml
318
319
  # => generates schemas-smrl-all.yml
319
320
  ----
320
321
 
321
322
  .To generate schemas manifest file from Metanorma manifest file and exclude schemas with names like `*_lf.exp`
322
323
  [source,sh]
323
324
  ----
324
- $ bundle exec suma generate_schemas metanorma-smrl-all.yml schemas-smrl-all.yml -e *_lf.exp
325
+ $ bundle exec suma generate-schemas metanorma-smrl-all.yml schemas-smrl-all.yml -e *_lf.exp
325
326
  # => generates schemas-smrl-all.yml without schemas with names like *_lf.exp
326
327
  ----
327
328
  ====
@@ -336,14 +337,14 @@ The "extract terms" command is implemented for ISO 10303-2, and could also be
336
337
  used for other EXPRESS schema collections that require term extraction for
337
338
  glossary or dictionary applications.
338
339
 
339
- The `suma extract_terms` command extracts terms from EXPRESS schemas and
340
+ The `suma extract-terms` command extracts terms from EXPRESS schemas and
340
341
  generates a Glossarist v2 dataset in the output directory. This command processes
341
342
  various types of STEP schemas and creates standardized terminology datasets
342
343
  suitable for glossary and dictionary applications.
343
344
 
344
345
  [source,sh]
345
346
  ----
346
- $ suma extract_terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH [options]
347
+ $ suma extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH [options]
347
348
  ----
348
349
 
349
350
  Parameters:
@@ -397,7 +398,7 @@ The command generates a Glossarist v2 compliant dataset with:
397
398
  ====
398
399
  [source,sh]
399
400
  ----
400
- $ bundle exec suma extract_terms schemas-smrl-all.yml glossarist_output
401
+ $ bundle exec suma extract-terms schemas-smrl-all.yml glossarist_output
401
402
  # => generates glossarist_output/concept/*.yaml and
402
403
  # glossarist_output/localized_concept/*.yaml
403
404
  ----
@@ -408,12 +409,144 @@ $ bundle exec suma extract_terms schemas-smrl-all.yml glossarist_output
408
409
  ====
409
410
  [source,sh]
410
411
  ----
411
- $ bundle exec suma extract_terms schemas-activity-modules.yml terms_output
412
+ $ bundle exec suma extract-terms schemas-activity-modules.yml terms_output
412
413
  # => processes only schemas listed in the manifest file
413
414
  ----
414
415
  ====
415
416
 
416
417
 
418
+ === Convert JSDAI image outputs to SVG and Annotated EXPRESS
419
+
420
+ The `suma convert-jsdai` command converts JSDAI EXPRESS-G diagram outputs
421
+ (XML + image file) into SVG and Annotated EXPRESS formats suitable for
422
+ Metanorma documentation.
423
+
424
+ JSDAI (Java Step Data Access Interface) Java tool is used by ISO/TC 184/SC 4 to
425
+ create EXPRESS-G diagrams.
426
+
427
+ JSDAI generates two files for each diagram:
428
+
429
+ * a raster image file (GIF or JPEG)
430
+ * an XML file containing image metadata and clickable area definitions
431
+
432
+ This command converts these inputs into:
433
+
434
+ * an SVG file that embeds the raster image as Base64 with clickable rectangular areas
435
+ * an Annotated EXPRESS file with a Metanorma `svgmap` block for easy copy-paste
436
+
437
+ [source,sh]
438
+ ----
439
+ $ suma convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR
440
+ ----
441
+
442
+ Where:
443
+
444
+ `XML_FILE`:: Path to the JSDAI XML file (e.g., "action_schemaexpg1.xml")
445
+
446
+ `IMAGE_FILE`:: Path to the raster image file (GIF or JPEG format)
447
+
448
+ `OUTPUT_DIR`:: Path to the output directory where SVG and EXP files will be generated
449
+
450
+ [example]
451
+ .To convert JSDAI outputs for a resource schema diagram
452
+ ====
453
+ [source,sh]
454
+ ----
455
+ $ bundle exec suma convert-jsdai \
456
+ documents/resources/action_schema/action_schemaexpg1.xml \
457
+ documents/resources/action_schema/action_schemaexpg1.gif \
458
+ output/
459
+ # => generates:
460
+ # output/action_schemaexpg1.svg
461
+ # output/action_schemaexpg1.exp
462
+ ----
463
+ ====
464
+
465
+ This command:
466
+
467
+ * Parses the JSDAI XML file to extract image metadata and clickable area definitions
468
+ * Reads the raster image file and converts it to base64 format
469
+ * Generates an SVG file with:
470
+ ** The embedded base64-encoded image
471
+ ** Clickable rectangular areas (`<a>` and `<rect>` elements) corresponding to the XML definitions
472
+ ** Proper viewBox dimensions matching the source image
473
+ * Generates an Annotated EXPRESS file containing:
474
+ ** A Metanorma `svgmap` block with numbered cross-references
475
+ ** Proper anchor IDs for document integration
476
+ ** Cross-reference targets extracted from the XML href attributes
477
+
478
+
479
+ The generated SVG and EXP files work together through a numbered mapping system:
480
+
481
+ . In the SVG file, each clickable area is assigned a sequential number:
482
+ +
483
+ [source,xml]
484
+ ----
485
+ <a href="1"><rect .../></a>
486
+ <a href="2"><rect .../></a>
487
+ <a href="3"><rect .../></a>
488
+ ----
489
+
490
+ . In the EXPRESS file, the `svgmap` block maps these numbers to targets which
491
+ are either EXPRESS or AsciiDoc anchors.
492
+ +
493
+ --
494
+ [example]
495
+ .Resource schema diagram with SVG and Annotated EXPRESS cross-references
496
+ =====
497
+ [source,express]
498
+ ----
499
+ (*"action_schema.__expressg"
500
+ [[action_schema_expg1]]
501
+ [.svgmap]
502
+ ====
503
+ image::action_schemaexpg1.svg[]
504
+
505
+ * <<express:basic_attribute_schema>>; 1
506
+ * <<express:action_schema>>; 2
507
+ * <<express:support_resource_schema>>; 3
508
+ ====
509
+ *)
510
+ ----
511
+ =====
512
+
513
+ [example]
514
+ .Module schema diagram with SVG and Annotated EXPRESS cross-references
515
+ =====
516
+ [source,express]
517
+ ----
518
+ (*"Activity_mim.__expressg"
519
+ [[Activity_mim_expg1]]
520
+ [.svgmap]
521
+ ====
522
+ image::mimexpg1.svg[]
523
+
524
+ * <<Activity_mim_expg2>>; 1
525
+ * <<express:action_schema>>; 2
526
+ * <<Activity_method_mim_expg1>>; 3
527
+ * <<express:basic_attribute_schema>>; 4
528
+ * <<express:management_resources_schema>>; 5
529
+ ====
530
+ *)
531
+ ----
532
+ =====
533
+ --
534
+
535
+ . When rendered in Metanorma, clicking on "area 1" in the SVG, will navigate to
536
+ the `express:basic_attribute_schema` anchor, "area 2" to
537
+ `express:action_schema`, and so on.
538
+
539
+ The mapping is derived from the original JSDAI XML file, where each `<img.area>`
540
+ element contains:
541
+
542
+ `coords` attribute:: converted to SVG `<rect>` dimensions
543
+
544
+ `href` attribute:: converted to EXPRESS cross-reference target in the `svgmap`
545
+ block
546
+
547
+ Sequential position:: assigned as the numbered href in both SVG and `svgmap`
548
+ list
549
+
417
550
 
418
551
  == Usage: Ruby
419
552
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../thor_ext"
5
+ require_relative "../jsdai"
6
+ require "fileutils"
7
+
8
+ module Suma
9
+ module Cli
10
+ # ConvertJsdai command to convert JSDAI XML and image to SVG and EXP
11
+ class ConvertJsdai < Thor
12
+ desc "convert_jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
13
+ "Convert JSDAI XML and image files to SVG and EXP files"
14
+
15
+ # rubocop:disable Metrics/MethodLength
16
+ def convert_jsdai(xml_file, image_file, output_dir)
17
+ xml_file = File.expand_path(xml_file)
18
+ image_file = File.expand_path(image_file)
19
+ output_dir = File.expand_path(output_dir)
20
+
21
+ unless File.exist?(xml_file)
22
+ raise Errno::ENOENT, "XML file not found: #{xml_file}"
23
+ end
24
+
25
+ unless File.exist?(image_file)
26
+ raise Errno::ENOENT, "Image file not found: #{image_file}"
27
+ end
28
+
29
+ unless File.file?(xml_file)
30
+ raise ArgumentError, "Specified path is not a file: #{xml_file}"
31
+ end
32
+
33
+ unless File.file?(image_file)
34
+ raise ArgumentError, "Specified path is not a file: #{image_file}"
35
+ end
36
+
37
+ run(xml_file, image_file, output_dir)
38
+ end
39
+ # rubocop:enable Metrics/MethodLength
40
+
41
+ private
42
+
43
+ # rubocop:disable Metrics/MethodLength
44
+ def run(xml_file, image_file, output_dir)
45
+ FileUtils.mkdir_p(output_dir)
46
+
47
+ figure = Suma::Jsdai::Figure.new(xml_file, image_file)
48
+ basename = File.basename(xml_file, ".xml")
49
+
50
+ svg_output = File.join(output_dir, "#{basename}.svg")
51
+ exp_output = File.join(output_dir, "#{basename}.exp")
52
+
53
+ puts "Converting JSDAI files..."
54
+ File.write(svg_output, figure.to_svg)
55
+ puts "Generated SVG: #{svg_output}"
56
+
57
+ File.write(exp_output, figure.to_exp)
58
+ puts "Generated EXP: #{exp_output}"
59
+
60
+ puts "Conversion complete."
61
+ end
62
+ # rubocop:enable Metrics/MethodLength
63
+ end
64
+ end
65
+ end
@@ -2,9 +2,10 @@
2
2
 
3
3
  require "thor"
4
4
  require "yaml"
5
- require "terminal-table"
5
+ require "paint"
6
6
  require "plurimath"
7
7
  require "set" # For using Set in unique_character_count
8
+ require "table_tennis"
8
9
  require_relative "../thor_ext"
9
10
 
10
11
  module Suma
@@ -188,12 +189,18 @@ module Suma
188
189
 
189
190
  # Print each file's violations
190
191
  @file_violations.each_value do |file_violation|
191
- puts "\n#{file_violation.display_path}:"
192
+ puts "\n#{Paint[file_violation.display_path, :cyan, :bold]}:"
192
193
 
193
194
  file_violation.violations.each do |v|
194
- puts " Line #{v[:line_number]}, Column #{v[:column]}:"
195
+ puts " #{Paint['Line',
196
+ :blue]} #{Paint[v[:line_number],
197
+ :yellow]}, #{Paint['Column',
198
+ :blue]} #{Paint[v[:column],
199
+ :yellow]}:"
195
200
  puts " #{v[:line]}"
196
- puts " #{' ' * v[:column]}#{'^' * v[:match].length} Non-ASCII sequence"
201
+ puts " #{' ' * v[:column]}#{Paint['^' * v[:match].length,
202
+ :red]} #{Paint['Non-ASCII sequence',
203
+ :red]}"
197
204
 
198
205
  v[:char_details].each do |cd|
199
206
  character = file_violation.unique_characters.find do |c|
@@ -201,29 +208,42 @@ module Suma
201
208
  end
202
209
  next unless character
203
210
 
204
- puts " \"#{cd[:char]}\" - Hex: #{cd[:hex]}, UTF-8 bytes: #{cd[:utf8]}"
205
- puts " Replacement: #{character.replacement_text}"
211
+ puts " #{Paint["\"#{cd[:char]}\"",
212
+ :yellow]} - Hex: #{Paint[cd[:hex],
213
+ :magenta]}, UTF-8 bytes: #{Paint[cd[:utf8],
214
+ :magenta]}"
215
+ puts " #{Paint['Replacement:',
216
+ :green]} #{character.replacement_text}"
206
217
  end
207
218
  puts ""
208
219
  end
209
220
 
210
- puts " Found #{file_violation.violation_count} non-ASCII sequence(s) in #{file_violation.filename}\n"
221
+ puts " #{Paint['Found',
222
+ :green]} #{Paint[file_violation.violation_count,
223
+ :red]} #{Paint['non-ASCII sequence(s) in',
224
+ :green]} #{Paint[file_violation.filename,
225
+ :cyan]}\n"
211
226
  end
212
227
 
213
228
  # Print summary
214
- puts "\nSummary:"
215
- puts " Scanned #{@total_files} EXPRESS file(s)"
216
- puts " Found #{total_violations} non-ASCII sequence(s) in #{files_with_violations} file(s)"
229
+ puts "\n#{Paint['Summary:', :blue, :bold]}"
230
+ puts " #{Paint['Scanned',
231
+ :green]} #{Paint[@total_files,
232
+ :yellow]} #{Paint['EXPRESS file(s)',
233
+ :green]}"
234
+ puts " #{Paint['Found',
235
+ :green]} #{Paint[total_violations,
236
+ :red]} #{Paint['non-ASCII sequence(s) in',
237
+ :green]} #{Paint[files_with_violations,
238
+ :red]} #{Paint['file(s)',
239
+ :green]}"
217
240
  end
218
241
 
219
242
  def print_table_output
220
243
  return if @file_violations.empty?
221
244
 
222
- table = ::Terminal::Table.new(
223
- title: "Non-ASCII Characters Summary",
224
- headings: ["File", "Symbol", "Replacement", "Occurrences"],
225
- )
226
-
245
+ # Build rows array
246
+ rows = []
227
247
  total_occurrences = 0
228
248
 
229
249
  @file_violations.each_value do |file_violation|
@@ -231,25 +251,37 @@ module Suma
231
251
  occurrence_count = character.occurrence_count
232
252
  total_occurrences += occurrence_count
233
253
 
234
- table.add_row [
235
- file_violation.display_path,
236
- "\"#{character.char}\" (#{character.hex})",
237
- character.replacement_text,
238
- occurrence_count,
239
- ]
254
+ rows << {
255
+ file: file_violation.display_path,
256
+ symbol: "\"#{character.char}\" (#{character.hex})",
257
+ replacement: character.replacement_text,
258
+ occurrences: occurrence_count,
259
+ }
240
260
  end
241
261
  end
242
262
 
243
- # Add a separator and total row
244
- table.add_separator
245
- table.add_row [
246
- "TOTAL",
247
- "#{unique_character_count} unique",
248
- "",
249
- total_occurrences,
250
- ]
263
+ # Add total row
264
+ rows << {
265
+ file: "TOTAL",
266
+ symbol: "#{unique_character_count} unique",
267
+ replacement: "",
268
+ occurrences: total_occurrences,
269
+ }
270
+
271
+ # Use TableTennis to render
272
+ options = {
273
+ title: "Non-ASCII Characters Summary",
274
+ columns: %i[file symbol replacement occurrences],
275
+ headers: {
276
+ file: "File",
277
+ symbol: "Symbol",
278
+ replacement: "Replacement",
279
+ occurrences: "Occurrences",
280
+ },
281
+ mark: ->(row) { row[:file] == "TOTAL" },
282
+ }
251
283
 
252
- puts "\n#{table}\n"
284
+ puts "\n#{TableTennis.new(rows, options)}\n"
253
285
  end
254
286
 
255
287
  private
data/lib/suma/cli.rb CHANGED
@@ -23,7 +23,7 @@ module Suma
23
23
  Cli::Build.start
24
24
  end
25
25
 
26
- desc "generate_schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE",
26
+ desc "generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE",
27
27
  "Generate EXPRESS schema manifest file from Metanorma site manifest"
28
28
  option :exclude_paths, type: :string, default: nil, aliases: "-e",
29
29
  desc: "Exclude schemas paths by pattern " \
@@ -43,7 +43,7 @@ module Suma
43
43
  Cli::Reformat.start
44
44
  end
45
45
 
46
- desc "extract_terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
46
+ desc "extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
47
47
  "Extract terms from SCHEMA_MANIFEST_FILE into " \
48
48
  "Glossarist v2 format"
49
49
  option :language_code, type: :string, default: "eng", aliases: "-l",
@@ -53,6 +53,13 @@ module Suma
53
53
  Cli::ExtractTerms.start
54
54
  end
55
55
 
56
+ desc "convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
57
+ "Convert JSDAI XML and image files to SVG and EXP files"
58
+ def convert_jsdai(_xml_file, _image_file, _output_dir)
59
+ require_relative "cli/convert_jsdai"
60
+ Cli::ConvertJsdai.start
61
+ end
62
+
56
63
  desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
57
64
  subcommand "validate", Cli::Validate
58
65
 
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "figure_xml"
4
+ require_relative "figure_image"
5
+
6
+ module Suma
7
+ module Jsdai
8
+ # Main class for JSDAI figure conversion
9
+ class Figure
10
+ attr_reader :xml, :image
11
+
12
+ def initialize(xml_file, image_file)
13
+ @xml = FigureXml.from_xml(File.read(xml_file))
14
+ @image = FigureImage.new(image_file)
15
+ @xml_file = xml_file
16
+ end
17
+
18
+ # rubocop:disable Metrics/MethodLength
19
+ def to_svg
20
+ width, height = @image.dimensions
21
+ svg_parts = []
22
+
23
+ svg_parts << '<?xml version="1.0" encoding="UTF-8"?>'
24
+ svg_parts << '<svg xmlns="http://www.w3.org/2000/svg" '
25
+ svg_parts << 'xml:space="preserve" '
26
+ svg_parts << 'style="enable-background:new 0 0 595.28 841.89;" '
27
+ svg_parts << "height=\"#{height}\" "
28
+ svg_parts << "width=\"#{width}\" "
29
+ svg_parts << "viewBox=\"0 0 #{width} #{height}\" "
30
+ svg_parts << 'y="0px" x="0px" id="Layer_1" version="1.1">'
31
+ svg_parts << "\n\t\t\t\n\t\t\t"
32
+
33
+ # Add embedded image
34
+ svg_parts << "<image href=\"#{@image.to_base64}\" "
35
+ svg_parts << "height=\"#{height}\" "
36
+ svg_parts << "width=\"#{width}\" "
37
+ svg_parts << 'style="overflow:visible;">'
38
+ svg_parts << "\n\t\t\t</image>\n\t\t\t"
39
+
40
+ # Add clickable areas
41
+ if @xml.img.areas && !@xml.img.areas.empty?
42
+ area_parts = @xml.img.areas.each_with_index.map do |area, index|
43
+ shape_element = generate_shape_element(area)
44
+ "<a href=\"#{index + 1}\">#{shape_element}</a>"
45
+ end
46
+
47
+ svg_parts << area_parts.join
48
+ end
49
+ svg_parts << "\n\t\t</svg>"
50
+ svg_parts.join
51
+ end
52
+ # rubocop:enable Metrics/MethodLength
53
+
54
+ # rubocop:disable Metrics/MethodLength
55
+ def to_exp
56
+ basename = File.basename(@xml_file, ".xml")
57
+ schema_name = extract_schema_name(basename)
58
+ anchor_id = extract_anchor_id(basename)
59
+
60
+ exp_parts = []
61
+ exp_parts << "(*\"#{schema_name}.__expressg\""
62
+ exp_parts << "\n[[#{anchor_id}]]"
63
+ exp_parts << "\n[.svgmap]"
64
+ exp_parts << "\n===="
65
+ exp_parts << "\nimage::#{basename}.svg[]"
66
+
67
+ if @xml.img.areas && !@xml.img.areas.empty?
68
+ exp_parts << "\n"
69
+ @xml.img.areas.each_with_index do |area, index|
70
+ target = extract_target_from_href(area.href)
71
+ exp_parts << "\n* <<#{target}>>; #{index + 1}"
72
+ end
73
+ end
74
+
75
+ exp_parts << "\n===="
76
+ exp_parts << "\n*)\n"
77
+ exp_parts.join
78
+ end
79
+ # rubocop:enable Metrics/MethodLength
80
+
81
+ private
82
+
83
+ # rubocop:disable Metrics/MethodLength
84
+ def generate_shape_element(area)
85
+ shape_attrs = []
86
+ shape_attrs << 'onmouseout="this.style.opacity=0" '
87
+ shape_attrs << 'onmouseover="this.style.opacity=1" '
88
+ shape_attrs << 'style="opacity: 0; fill: rgb(33, 128, 255); '
89
+ shape_attrs << "fill-opacity: 0.3; stroke: rgb(0, 128, 255); "
90
+ shape_attrs << "stroke-width: 1px; stroke-linecap: butt; "
91
+ shape_attrs << 'stroke-linejoin: miter; stroke-opacity: 1;" '
92
+
93
+ case area.shape
94
+ when "rect"
95
+ coords = parse_rect_coords(area.coords)
96
+ shape_attrs << "height=\"#{coords[:height]}\" "
97
+ shape_attrs << "width=\"#{coords[:width]}\" "
98
+ shape_attrs << "y=\"#{coords[:y]}\" "
99
+ shape_attrs << "x=\"#{coords[:x]}\"/>"
100
+ "<rect #{shape_attrs.join}"
101
+ when "poly", "polygon"
102
+ shape_attrs << "points=\"#{area.coords}\"/>"
103
+ "<polygon #{shape_attrs.join}"
104
+ else
105
+ # Unsupported shape, default to rectangle
106
+ coords = parse_rect_coords(area.coords)
107
+ shape_attrs << "height=\"#{coords[:height]}\" "
108
+ shape_attrs << "width=\"#{coords[:width]}\" "
109
+ shape_attrs << "y=\"#{coords[:y]}\" "
110
+ shape_attrs << "x=\"#{coords[:x]}\"/>"
111
+ "<rect #{shape_attrs.join}"
112
+ end
113
+ end
114
+ # rubocop:enable Metrics/MethodLength
115
+
116
+ def parse_rect_coords(coords_str)
117
+ parts = coords_str.split(",").map(&:to_i)
118
+ {
119
+ x: parts[0],
120
+ y: parts[1],
121
+ width: parts[2] - parts[0],
122
+ height: parts[3] - parts[1],
123
+ }
124
+ end
125
+
126
+ def parse_coords(coords_str)
127
+ parse_rect_coords(coords_str)
128
+ end
129
+
130
+ def extract_anchor_id(basename)
131
+ # For module schemas like "armexpg1" with module="activity"
132
+ # Result should be "Activity_arm_expg1"
133
+ # For resource schemas like "action_schemaexpg2"
134
+ # Result should be "action_schema_expg2"
135
+
136
+ if basename =~ /^(arm|mim)expg(\d+)$/
137
+ # Module schema: use schema_name + _expg + number
138
+ schema_name = extract_schema_name(basename)
139
+ "#{schema_name}_expg#{::Regexp.last_match(2)}"
140
+ else
141
+ # Resource schema: insert underscore before expg
142
+ basename.sub("expg", "_expg")
143
+ end
144
+ end
145
+
146
+ # rubocop:disable Metrics/MethodLength
147
+ def extract_schema_name(basename)
148
+ # For module schemas like "armexpg1" with module="activity"
149
+ # Result should be "Activity_arm"
150
+ # For resource schemas like "action_schemaexpg2"
151
+ # Result should be "action_schema"
152
+
153
+ if basename =~ /^(arm|mim)expg\d+$/
154
+ # Module schema: use XML module attribute + arm/mim
155
+ # Capitalize only the first letter of the module name
156
+ module_name = @xml.module.split("_").map.with_index do |part, idx|
157
+ idx.zero? ? part.capitalize : part
158
+ end.join("_")
159
+ "#{module_name}_#{::Regexp.last_match(1)}"
160
+ else
161
+ # Resource schema: strip expg suffix
162
+ basename.sub(/expg\d+$/, "")
163
+ end
164
+ end
165
+ # rubocop:enable Metrics/MethodLength
166
+
167
+ # rubocop:disable Metrics/MethodLength
168
+ def extract_target_from_href(href)
169
+ # Extract the target from href
170
+ # Type 1: "../../resources/action_schema/action_schema.xml#action_schema.as_name_attribute_select"
171
+ # → "express:action_schema.as_name_attribute_select"
172
+ # Type 2: "../../resources/basic_attribute_schema/basic_attribute_schema.xml"
173
+ # → "express:basic_attribute_schema"
174
+ # Type 3: "../activity_method/armexpg1.xml"
175
+ # → "Activity_method_arm_expg1"
176
+ # Type 4: "../../resources/geometry_schema/geometry_schemaexpg3.xml"
177
+ # → "geometry_schema_expg3" (image reference in same resource)
178
+
179
+ case href
180
+ when /#(.+)$/
181
+ # Has fragment - use it as entity reference
182
+ "express:#{::Regexp.last_match(1)}"
183
+ when %r{^\.\./([\w_]+)/(arm|mim)expg(\d+)\.xml$}
184
+ # Module image reference like "../activity_method/armexpg1.xml"
185
+ module_dir = ::Regexp.last_match(1)
186
+ schema_type = ::Regexp.last_match(2)
187
+ expg_num = ::Regexp.last_match(3)
188
+ # Capitalize only the first letter of the module name
189
+ module_name = module_dir.split("_").map.with_index do |part, idx|
190
+ idx.zero? ? part.capitalize : part
191
+ end.join("_")
192
+ "#{module_name}_#{schema_type}_expg#{expg_num}"
193
+ when %r{/([^/]+)expg(\d+)\.xml$}
194
+ # Image reference to another diagram in same or different resource
195
+ # e.g., "../../resources/geometry_schema/geometry_schemaexpg3.xml"
196
+ # Result: "geometry_schema_expg3" (no "express:" prefix for images)
197
+ schema_name = ::Regexp.last_match(1)
198
+ expg_num = ::Regexp.last_match(2)
199
+ "#{schema_name}_expg#{expg_num}"
200
+ when %r{/([^/]+)\.xml$}
201
+ # Resource schema reference (no expg)
202
+ "express:#{::Regexp.last_match(1)}"
203
+ else
204
+ href
205
+ end
206
+ end
207
+ # rubocop:enable Metrics/MethodLength
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Suma
6
+ module Jsdai
7
+ # Represents a JSDAI figure image file
8
+ class FigureImage
9
+ attr_reader :path, :width, :height
10
+
11
+ def initialize(image_file)
12
+ @path = image_file
13
+ @base64_data = nil
14
+ @width = nil
15
+ @height = nil
16
+ @image_type = extract_image_type
17
+ end
18
+
19
+ def to_base64
20
+ @to_base64 ||= begin
21
+ image_data = File.binread(@path)
22
+ "data:image/#{@image_type};base64,#{Base64.strict_encode64(image_data)}"
23
+ end
24
+ end
25
+
26
+ def dimensions
27
+ extract_dimensions unless @width && @height
28
+ [@width, @height]
29
+ end
30
+
31
+ private
32
+
33
+ def extract_image_type
34
+ File.extname(@path).delete(".").downcase
35
+ end
36
+
37
+ def extract_dimensions
38
+ case @image_type
39
+ when "gif"
40
+ extract_gif_dimensions
41
+ when "jpg", "jpeg"
42
+ extract_jpeg_dimensions
43
+ else
44
+ raise "Unsupported image type: #{@image_type}"
45
+ end
46
+ end
47
+
48
+ def extract_gif_dimensions
49
+ # Read GIF header to extract dimensions
50
+ # GIF87a and GIF89a format: width at bytes 6-7, height at bytes 8-9
51
+ File.open(@path, "rb") do |file|
52
+ file.read(6) # Skip "GIF87a" or "GIF89a"
53
+ width_bytes = file.read(2)
54
+ height_bytes = file.read(2)
55
+
56
+ @width = width_bytes.unpack1("S<") # Little-endian short
57
+ @height = height_bytes.unpack1("S<")
58
+ end
59
+ end
60
+
61
+ # rubocop:disable Metrics/MethodLength
62
+ def extract_jpeg_dimensions
63
+ # Read JPEG file to extract dimensions
64
+ # JPEG uses markers, we look for SOF (Start of Frame) markers
65
+ File.open(@path, "rb") do |file|
66
+ # Check for JPEG magic number
67
+ return unless file.read(2) == "\xFF\xD8".b
68
+
69
+ loop do
70
+ marker = file.read(2)
71
+ break unless marker
72
+
73
+ # SOF markers: 0xFFC0-0xFFC3, 0xFFC5-0xFFC7, 0xFFC9-0xFFCB, 0xFFCD-0xFFCF
74
+ if marker[0] == "\xFF".b &&
75
+ marker[1].ord.between?(0xC0, 0xCF) &&
76
+ marker[1].ord != 0xC4 && marker[1].ord != 0xC8 && marker[1].ord != 0xCC
77
+ file.read(3) # Skip length (2 bytes) and precision (1 byte)
78
+ height_bytes = file.read(2)
79
+ width_bytes = file.read(2)
80
+ @height = height_bytes.unpack1("n") # Big-endian short
81
+ @width = width_bytes.unpack1("n")
82
+ break
83
+ end
84
+
85
+ # Skip this marker's data
86
+ length = file.read(2)&.unpack1("n")
87
+ break unless length
88
+
89
+ file.seek(length - 2, IO::SEEK_CUR)
90
+ end
91
+ end
92
+ end
93
+ # rubocop:enable Metrics/MethodLength
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Suma
6
+ module Jsdai
7
+ # Represents an image area with coordinates and href
8
+ class FigureXmlImageArea < Lutaml::Model::Serializable
9
+ attribute :shape, :string
10
+ attribute :coords, :string
11
+ attribute :href, :string
12
+
13
+ xml do
14
+ root "img.area"
15
+ map_attribute "shape", to: :shape
16
+ map_attribute "coords", to: :coords
17
+ map_attribute "href", to: :href
18
+ end
19
+ end
20
+
21
+ # Represents the img element with source and areas
22
+ class FigureXmlImage < Lutaml::Model::Serializable
23
+ attribute :src, :string
24
+ attribute :areas, FigureXmlImageArea, collection: true
25
+
26
+ xml do
27
+ root "img"
28
+ map_attribute "src", to: :src
29
+ map_element "img.area", to: :areas
30
+ end
31
+ end
32
+
33
+ # Represents the root imgfile.content element
34
+ class FigureXml < Lutaml::Model::Serializable
35
+ attribute :module, :string
36
+ attribute :file, :string
37
+ attribute :img, FigureXmlImage
38
+
39
+ xml do
40
+ root "imgfile.content"
41
+ map_attribute "module", to: :module
42
+ map_attribute "file", to: :file
43
+ map_element "img", to: :img
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/suma/jsdai.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Suma
2
+ module Jsdai
3
+ end
4
+ end
5
+
6
+ require_relative "jsdai/figure"
7
+
8
+ # Configure XML adapter to Nokogiri because Ox goes into a "stack level too
9
+ # deep" error, for unknown reasons
10
+ Lutaml::Model::Config.configure do |config|
11
+ config.xml_adapter_type = :nokogiri
12
+ end
data/lib/suma/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Suma
4
- VERSION = "0.1.20"
4
+ VERSION = "0.1.22"
5
5
  end
data/lib/suma.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "expressir"
4
+ require "lutaml/model"
5
+
4
6
  require_relative "suma/version"
5
7
  require_relative "suma/processor"
6
8
 
data/suma.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
39
39
  spec.add_dependency "metanorma-cli"
40
40
  spec.add_dependency "plurimath"
41
41
  spec.add_dependency "ruby-progressbar"
42
- spec.add_dependency "terminal-table", "~> 3.0"
42
+ spec.add_dependency "table_tennis"
43
43
  spec.add_dependency "thor", ">= 0.20"
44
44
  spec.metadata["rubygems_mfa_required"] = "true"
45
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.20
4
+ version: 0.1.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-18 00:00:00.000000000 Z
11
+ date: 2025-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir
@@ -95,19 +95,19 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: terminal-table
98
+ name: table_tennis
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '3.0'
103
+ version: '0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '3.0'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: thor
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -148,6 +148,7 @@ files:
148
148
  - lib/suma.rb
149
149
  - lib/suma/cli.rb
150
150
  - lib/suma/cli/build.rb
151
+ - lib/suma/cli/convert_jsdai.rb
151
152
  - lib/suma/cli/extract_terms.rb
152
153
  - lib/suma/cli/generate_schemas.rb
153
154
  - lib/suma/cli/reformat.rb
@@ -157,6 +158,10 @@ files:
157
158
  - lib/suma/collection_config.rb
158
159
  - lib/suma/collection_manifest.rb
159
160
  - lib/suma/express_schema.rb
161
+ - lib/suma/jsdai.rb
162
+ - lib/suma/jsdai/figure.rb
163
+ - lib/suma/jsdai/figure_image.rb
164
+ - lib/suma/jsdai/figure_xml.rb
160
165
  - lib/suma/processor.rb
161
166
  - lib/suma/schema_attachment.rb
162
167
  - lib/suma/schema_collection.rb