lutaml 0.10.5 → 0.10.7

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop_todo.yml +35 -61
  4. data/exe/lutaml +1 -0
  5. data/lib/lutaml/cli/interactive_shell/command_base.rb +4 -0
  6. data/lib/lutaml/cli/interactive_shell.rb +3 -3
  7. data/lib/lutaml/cli/uml/spa_command.rb +4 -0
  8. data/lib/lutaml/qea/factory/association_builder.rb +1 -0
  9. data/lib/lutaml/qea/factory/class_transformer.rb +4 -2
  10. data/lib/lutaml/qea/factory/generalization_builder.rb +2 -2
  11. data/lib/lutaml/uml/inheritance_walker.rb +1 -0
  12. data/lib/lutaml/uml/model_helpers.rb +1 -1
  13. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +3 -1
  14. data/lib/lutaml/uml_repository/exporters/markdown_exporter.rb +2 -1
  15. data/lib/lutaml/uml_repository/repository.rb +1 -0
  16. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +22 -6
  17. data/lib/lutaml/uml_repository/static_site/generator.rb +32 -9
  18. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +7 -2
  19. data/lib/lutaml/uml_repository/static_site/serializers/diagram_serializer.rb +22 -1
  20. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +12 -4
  21. data/lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb +10 -3
  22. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +6 -2
  23. data/lib/lutaml/version.rb +1 -1
  24. data/templates/static_site/assets/scripts/core/state.js +28 -1
  25. data/templates/static_site/assets/styles/06-diagrams.css +48 -0
  26. data/templates/static_site/components/content.liquid +73 -3
  27. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d1caee886e1e27cbbd7db3101f510bd0a1169050cbab47f48bdb31622e46c16
4
- data.tar.gz: 4d42b151859f65339b67dffc2e5070e2851819daaf145295b05285d2cecc5e50
3
+ metadata.gz: 84ecefe8a15ae033ee203318b3db70ac20ea3f727d4a94cb98ffdd6444f0111f
4
+ data.tar.gz: afe30e1675098ee1248114e4999117b0b8b159c902ae304e5d4b8212ac801bfa
5
5
  SHA512:
6
- metadata.gz: 62f845a38a4336cee141fb710ced850a20332db561ab44fe08d16c9e780c75c2844b982176902c0eab600175561e7efd7b63136ba9eabc707ce07d424f1c0c9f
7
- data.tar.gz: 0ce0fd5ae74fdd0ba9f795e39e4b7b0ead7492443f06eeee34dbbeaaf31fdab16601174dbf53f72d412c13581a8cac82cefdf4906c060f317d7f7afd2a8f3b1d
6
+ metadata.gz: 4ac75c14a3991c8b7885f4fc62fbe29640be5fe7b4d2cc9a5abfc01c585e038dd50a495a69cdab4483fb6840671b1a1ab35a66263e570c1da7cc005ce9fda626
7
+ data.tar.gz: 92f724a84ddfc10d49800a2f118bdc31d6d80054d9238a2feacc0a1f9e74312ea1fc96ac95b58893f5ec24d88f1fbe09f2b2eff64be6b45f879208babfa162b8
data/.gitignore CHANGED
@@ -8,9 +8,17 @@ Gemfile.lock
8
8
  # rspec failure tracking
9
9
  .rspec_status
10
10
  /spec/reports/
11
+ /spec/tmp/
11
12
  /tmp/
12
13
  /.yardoc
13
14
  /_yardoc/
14
15
 
15
16
  _site/
16
17
  old-docs
18
+
19
+ # Editor / OS
20
+ .DS_Store
21
+
22
+ # Transient planning files
23
+ TODO.*/
24
+ .rubocop_todo.yml.tmp
data/.rubocop_todo.yml CHANGED
@@ -1,51 +1,43 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-04-28 10:51:28 UTC using RuboCop version 1.86.1.
3
+ # on 2026-05-07 04:34:28 UTC using RuboCop version 1.86.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
- # Offense count: 16
10
- # This cop supports safe autocorrection (--autocorrect).
11
- Layout/EmptyLineAfterGuardClause:
12
- Exclude:
13
- - 'lib/lutaml/qea/factory/association_builder.rb'
14
- - 'lib/lutaml/uml/inheritance_walker.rb'
15
- - 'lib/lutaml/uml_repository/static_site/data_transformer.rb'
16
- - 'lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb'
17
- - 'lib/lutaml/uml_repository/static_site/serializers/diagram_serializer.rb'
18
- - 'lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb'
19
- - 'lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb'
20
-
21
- # Offense count: 4
9
+ # Offense count: 7
22
10
  # This cop supports safe autocorrection (--autocorrect).
23
- # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
24
- Layout/EmptyLineBetweenDefs:
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
25
14
  Exclude:
26
- - 'lib/lutaml/cli/interactive_shell/command_base.rb'
15
+ - 'lib/lutaml/uml_repository/static_site/generator.rb'
16
+ - 'spec/lutaml/uml_repository/static_site/serializers/diagram_serializer_spec.rb'
27
17
 
28
- # Offense count: 1
18
+ # Offense count: 6
29
19
  # This cop supports safe autocorrection (--autocorrect).
30
- Layout/EmptyLinesAfterModuleInclusion:
20
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
21
+ # SupportedHashRocketStyles: key, separator, table
22
+ # SupportedColonStyles: key, separator, table
23
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
24
+ Layout/HashAlignment:
31
25
  Exclude:
32
- - 'lib/lutaml/uml_repository/repository.rb'
26
+ - 'spec/lutaml/uml_repository/static_site/serializers/diagram_serializer_spec.rb'
33
27
 
34
- # Offense count: 183
28
+ # Offense count: 175
35
29
  # This cop supports safe autocorrection (--autocorrect).
36
30
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
37
31
  # URISchemes: http, https
38
32
  Layout/LineLength:
39
33
  Enabled: false
40
34
 
41
- # Offense count: 2
35
+ # Offense count: 3
42
36
  # This cop supports safe autocorrection (--autocorrect).
43
- # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
44
- # SupportedStyles: space, no_space, compact
45
- # SupportedStylesForEmptyBraces: space, no_space
46
- Layout/SpaceInsideHashLiteralBraces:
37
+ # Configuration parameters: AllowInHeredoc.
38
+ Layout/TrailingWhitespace:
47
39
  Exclude:
48
- - 'lib/lutaml/uml/model_helpers.rb'
40
+ - 'spec/lutaml/uml_repository/static_site/serializers/diagram_serializer_spec.rb'
49
41
 
50
42
  # Offense count: 1
51
43
  Lint/BinaryOperatorWithIdenticalOperands:
@@ -98,13 +90,6 @@ Lint/MissingSuper:
98
90
  Exclude:
99
91
  - 'lib/lutaml/uml_repository/lazy_repository.rb'
100
92
 
101
- # Offense count: 2
102
- # This cop supports safe autocorrection (--autocorrect).
103
- Lint/RedundantRequireStatement:
104
- Exclude:
105
- - 'lib/lutaml/qea/factory/generalization_builder.rb'
106
- - 'lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb'
107
-
108
93
  # Offense count: 1
109
94
  # This cop supports safe autocorrection (--autocorrect).
110
95
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
@@ -118,7 +103,7 @@ Lint/UselessConstantScoping:
118
103
  Exclude:
119
104
  - 'lib/lutaml/cli/interactive_shell.rb'
120
105
 
121
- # Offense count: 52
106
+ # Offense count: 51
122
107
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
123
108
  Metrics/AbcSize:
124
109
  Enabled: false
@@ -134,7 +119,7 @@ Metrics/BlockLength:
134
119
  Metrics/CyclomaticComplexity:
135
120
  Enabled: false
136
121
 
137
- # Offense count: 65
122
+ # Offense count: 67
138
123
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
139
124
  Metrics/MethodLength:
140
125
  Max: 62
@@ -209,7 +194,7 @@ RSpec/ContextWording:
209
194
  RSpec/DescribeClass:
210
195
  Enabled: false
211
196
 
212
- # Offense count: 649
197
+ # Offense count: 654
213
198
  # Configuration parameters: CountAsOne.
214
199
  RSpec/ExampleLength:
215
200
  Max: 30
@@ -263,7 +248,7 @@ RSpec/MessageSpies:
263
248
  - 'spec/lutaml/uml_repository/presenters/diagram_presenter_spec.rb'
264
249
  - 'spec/lutaml/uml_repository/repository_spec.rb'
265
250
 
266
- # Offense count: 35
251
+ # Offense count: 38
267
252
  RSpec/MultipleExpectations:
268
253
  Max: 6
269
254
 
@@ -285,6 +270,12 @@ RSpec/NoExpectationExample:
285
270
  - 'spec/lutaml/qea/verification/comprehensive_equivalence_spec.rb'
286
271
  - 'spec/lutaml/uml_repository/static_site/generator_spec.rb'
287
272
 
273
+ # Offense count: 6
274
+ # This cop supports unsafe autocorrection (--autocorrect-all).
275
+ RSpec/ReceiveMessages:
276
+ Exclude:
277
+ - 'spec/lutaml/uml_repository/static_site/serializers/diagram_serializer_spec.rb'
278
+
288
279
  # Offense count: 12
289
280
  RSpec/RepeatedExample:
290
281
  Exclude:
@@ -309,6 +300,12 @@ RSpec/SpecFilePathFormat:
309
300
  - 'spec/lutaml/parsers/serialize_xmi_to_liquid_spec.rb'
310
301
  - 'spec/lutaml/uml_repository/web_ui/app_spec.rb'
311
302
 
303
+ # Offense count: 10
304
+ # This cop supports unsafe autocorrection (--autocorrect-all).
305
+ RSpec/VerifiedDoubleReference:
306
+ Exclude:
307
+ - 'spec/lutaml/uml_repository/static_site/serializers/diagram_serializer_spec.rb'
308
+
312
309
  # Offense count: 3
313
310
  Security/Eval:
314
311
  Exclude:
@@ -336,13 +333,6 @@ Style/EvalWithLocation:
336
333
  Exclude:
337
334
  - 'spec/lutaml/cli/uml/search_command_spec.rb'
338
335
 
339
- # Offense count: 1
340
- # This cop supports safe autocorrection (--autocorrect).
341
- # Configuration parameters: AllowMethodComparison, ComparisonsThreshold.
342
- Style/MultipleComparison:
343
- Exclude:
344
- - 'lib/lutaml/cli/interactive_shell.rb'
345
-
346
336
  # Offense count: 1
347
337
  # This cop supports unsafe autocorrection (--autocorrect-all).
348
338
  Style/PartitionInsteadOfDoubleSelect:
@@ -355,19 +345,3 @@ Style/SafeNavigationChainLength:
355
345
  Exclude:
356
346
  - 'lib/lutaml/qea/validation/attribute_validator.rb'
357
347
  - 'lib/lutaml/qea/validation/operation_validator.rb'
358
-
359
- # Offense count: 1
360
- # This cop supports safe autocorrection (--autocorrect).
361
- # Configuration parameters: EnforcedStyleForMultiline.
362
- # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
363
- Style/TrailingCommaInArguments:
364
- Exclude:
365
- - 'lib/lutaml/qea/factory/class_transformer.rb'
366
-
367
- # Offense count: 2
368
- # This cop supports safe autocorrection (--autocorrect).
369
- # Configuration parameters: EnforcedStyleForMultiline.
370
- # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
371
- Style/TrailingCommaInHashLiteral:
372
- Exclude:
373
- - 'lib/lutaml/cli/interactive_shell.rb'
data/exe/lutaml CHANGED
@@ -5,6 +5,7 @@ require "pathname"
5
5
  bin_file = Pathname.new(__FILE__).realpath
6
6
  $:.unshift File.expand_path("../../lib", bin_file)
7
7
 
8
+ require "lutaml"
8
9
  require "lutaml/cli"
9
10
 
10
11
  Lutaml::CLI.start(ARGV)
@@ -12,15 +12,19 @@ module Lutaml
12
12
 
13
13
  def repository = shell.repository
14
14
  def current_path = shell.current_path
15
+
15
16
  def current_path=(path)
16
17
  shell.instance_variable_set(:@current_path, path)
17
18
  end
19
+
18
20
  def config = shell.config
19
21
  def bookmarks = shell.bookmarks
20
22
  def last_results = shell.last_results
23
+
21
24
  def last_results=(results)
22
25
  shell.instance_variable_set(:@last_results, results)
23
26
  end
27
+
24
28
  def path_history = shell.path_history
25
29
  end
26
30
  end
@@ -101,7 +101,7 @@ module Lutaml
101
101
  "history" => :@help,
102
102
  "clear" => :@help, "cls" => :@help,
103
103
  "config" => :@help,
104
- "stats" => :@help,
104
+ "stats" => :@help
105
105
  }.freeze
106
106
 
107
107
  METHOD_MAP = {
@@ -119,7 +119,7 @@ module Lutaml
119
119
  "history" => :cmd_history,
120
120
  "clear" => :cmd_clear, "cls" => :cmd_clear,
121
121
  "config" => :cmd_config,
122
- "stats" => :cmd_stats,
122
+ "stats" => :cmd_stats
123
123
  }.freeze
124
124
 
125
125
  def execute_command(input)
@@ -127,7 +127,7 @@ module Lutaml
127
127
  command = parts[0].downcase
128
128
  args = parts[1..]
129
129
 
130
- if command == "exit" || command == "quit" || command == "q"
130
+ if ["exit", "quit", "q"].include?(command)
131
131
  @running = false
132
132
  return
133
133
  end
@@ -47,6 +47,9 @@ module Lutaml
47
47
  desc: "Minify HTML/CSS/JS output"
48
48
  thor_class.option :theme, type: :string, default: "light",
49
49
  desc: "Default theme: 'light' or 'dark'"
50
+ thor_class.option :render_diagrams, type: :boolean, default: false,
51
+ desc: "Render diagram SVGs " \
52
+ "(may increase output size)"
50
53
  end
51
54
 
52
55
  def run(input_path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
@@ -169,6 +172,7 @@ module Lutaml
169
172
  title: options[:title] || "UML Model Browser",
170
173
  minify: options[:minify] || false,
171
174
  theme: options[:theme] || "light",
175
+ render_diagrams: options[:render_diagrams] || false,
172
176
  }
173
177
 
174
178
  Lutaml::UmlRepository::StaticSite.generate(repository,
@@ -179,6 +179,7 @@ module Lutaml
179
179
 
180
180
  def find_package_name(package_id)
181
181
  return nil if package_id.nil?
182
+
182
183
  database.find_package(package_id)&.name
183
184
  end
184
185
  end
@@ -60,7 +60,7 @@ module Lutaml
60
60
  .load_association_generalizations(ea_object.ea_object_id)
61
61
 
62
62
  klass.associations = assoc_builder.load_class_associations(
63
- ea_object.ea_object_id, ea_object.ea_guid,
63
+ ea_object.ea_object_id, ea_object.ea_guid
64
64
  )
65
65
  end
66
66
  end
@@ -116,7 +116,9 @@ module Lutaml
116
116
  return [] if ea_guid.nil?
117
117
  return [] unless database.tagged_values
118
118
 
119
- ea_tags = database.tagged_values.select { |tag| tag.element_id == ea_guid }
119
+ ea_tags = database.tagged_values.select do |tag|
120
+ tag.element_id == ea_guid
121
+ end
120
122
  TaggedValueTransformer.new(database).transform_collection(ea_tags)
121
123
  end
122
124
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require_relative "base_transformer"
5
4
  require_relative "attribute_transformer"
6
5
  require_relative "generalization_transformer"
@@ -31,7 +30,8 @@ module Lutaml
31
30
  generalization = if ea_connector.nil?
32
31
  gen_transformer.transform(nil, current_obj)
33
32
  else
34
- gen_transformer.transform(ea_connector, current_obj)
33
+ gen_transformer.transform(ea_connector,
34
+ current_obj)
35
35
  end
36
36
  return nil unless generalization
37
37
 
@@ -38,6 +38,7 @@ module Lutaml
38
38
  collect_ancestors(klass, ancestors)
39
39
  ancestors.reverse_each.with_index(1) do |ancestor, level|
40
40
  break if @visited.include?(ancestor.xmi_id)
41
+
41
42
  @visited.add(ancestor.xmi_id)
42
43
  yield(ancestor, level) if block_given?
43
44
  end
@@ -122,7 +122,7 @@ module Lutaml
122
122
  # @param max [String, nil]
123
123
  # @return [Hash{Symbol => String, nil}] Hash with :min and :max keys
124
124
  def parse_cardinality(min, max)
125
- {min: min, max: max}
125
+ { min: min, max: max }
126
126
  end
127
127
  end
128
128
  end
@@ -49,7 +49,9 @@ module Lutaml
49
49
  stereotypes_array = normalize_stereotypes(klass.stereotype)
50
50
  return "" if stereotypes_array.empty?
51
51
 
52
- "**Stereotypes**: #{stereotypes_array.map { |s| "`#{s}`" }.join(', ')}\n\n"
52
+ "**Stereotypes**: #{stereotypes_array.map do |s|
53
+ "`#{s}`"
54
+ end.join(', ')}\n\n"
53
55
  end
54
56
 
55
57
  def build_definition_section(klass)
@@ -41,7 +41,8 @@ module Lutaml
41
41
  end
42
42
 
43
43
  def generate_index_page
44
- content = Markdown::IndexPageBuilder.new(repository, options, link_resolver).build
44
+ content = Markdown::IndexPageBuilder.new(repository, options,
45
+ link_resolver).build
45
46
  File.write(File.join(output_dir, "index.md"), content)
46
47
  end
47
48
 
@@ -51,6 +51,7 @@ module Lutaml
51
51
  # descendants = repo.descendants_of("ModelRoot::Parent", max_depth: 2)
52
52
  class Repository
53
53
  include Deprecated
54
+
54
55
  # @return [Lutaml::Uml::Document] The underlying UML document
55
56
  attr_reader :document
56
57
 
@@ -32,13 +32,24 @@ module Lutaml
32
32
  def transform
33
33
  {
34
34
  metadata: Serializers::MetadataBuilder.new(repository).build,
35
- packageTree: Serializers::PackageTreeBuilder.new(repository, id_generator).build,
36
- packages: Serializers::PackageSerializer.new(repository, id_generator, options).build_map,
37
- classes: Serializers::ClassSerializer.new(repository, id_generator, options, inheritance_resolver).build_map,
38
- attributes: Serializers::AttributeSerializer.new(repository, id_generator, options).build_map,
35
+ packageTree: Serializers::PackageTreeBuilder.new(repository,
36
+ id_generator).build,
37
+ packages: Serializers::PackageSerializer.new(repository,
38
+ id_generator, options).build_map,
39
+ classes: Serializers::ClassSerializer.new(repository, id_generator,
40
+ options, inheritance_resolver).build_map,
41
+ attributes: Serializers::AttributeSerializer.new(repository,
42
+ id_generator, options).build_map,
39
43
  associations: build_associations_map,
40
- operations: Serializers::OperationSerializer.new(repository, id_generator).build_map,
41
- diagrams: (options[:include_diagrams] ? Serializers::DiagramSerializer.new(repository, id_generator, options).build_map : {}),
44
+ operations: Serializers::OperationSerializer.new(repository,
45
+ id_generator).build_map,
46
+ diagrams: (if options[:include_diagrams]
47
+ Serializers::DiagramSerializer.new(
48
+ repository, id_generator, options
49
+ ).build_map
50
+ else
51
+ {}
52
+ end),
42
53
  }
43
54
  end
44
55
 
@@ -69,12 +80,14 @@ module Lutaml
69
80
 
70
81
  klass.association_generalization.each do |assoc_gen|
71
82
  next unless assoc_gen.respond_to?(:parent_object_id)
83
+
72
84
  parent_object_id = assoc_gen.parent_object_id
73
85
  next unless parent_object_id
74
86
 
75
87
  parent_class = class_lookup.by_object_id(parent_object_id)
76
88
  if parent_class&.xmi_id
77
89
  next if parent_class.xmi_id == klass.xmi_id
90
+
78
91
  unless map[klass.xmi_id].include?(parent_class.xmi_id)
79
92
  map[klass.xmi_id] << parent_class.xmi_id
80
93
  end
@@ -91,6 +104,7 @@ module Lutaml
91
104
 
92
105
  def find_class_by_xmi_id(xmi_id)
93
106
  return nil unless xmi_id
107
+
94
108
  class_lookup.by_xmi_id(xmi_id)
95
109
  rescue StandardError
96
110
  nil
@@ -98,6 +112,7 @@ module Lutaml
98
112
 
99
113
  def find_class_by_object_id(object_id)
100
114
  return nil unless object_id
115
+
101
116
  class_lookup.by_object_id(object_id)
102
117
  rescue StandardError
103
118
  nil
@@ -114,6 +129,7 @@ module Lutaml
114
129
 
115
130
  def format_definition(definition)
116
131
  return nil if definition.nil? || definition.empty?
132
+
117
133
  formatted = definition.strip
118
134
  if @options[:max_definition_length] &&
119
135
  formatted.length > @options[:max_definition_length]
@@ -11,6 +11,27 @@ require_relative "id_generator"
11
11
  module Lutaml
12
12
  module UmlRepository
13
13
  module StaticSite
14
+ # Resolves Liquid {% include %} paths to template files on disk.
15
+ # Unlike LocalFileSystem (which adds a "_" prefix), this resolves
16
+ # paths directly: "components/header" → "<root>/components/header.liquid"
17
+ class TemplateFileSystem
18
+ attr_reader :root
19
+
20
+ def initialize(root)
21
+ @root = root
22
+ end
23
+
24
+ def read_template_file(template_path)
25
+ full = File.expand_path("#{template_path}.liquid", @root)
26
+ unless full.start_with?(@root)
27
+ raise Liquid::FileSystemError,
28
+ "Illegal template path: #{template_path}"
29
+ end
30
+
31
+ File.read(full)
32
+ end
33
+ end
34
+
14
35
  # Main static site generator for LutaML UML Browser.
15
36
  #
16
37
  # Follows Dependency Inversion Principle by injecting dependencies
@@ -143,7 +164,8 @@ module Lutaml
143
164
  include_diagrams: config_opts["include_diagrams"] != false,
144
165
  format_definitions: config_opts["format_definitions"] != false,
145
166
  max_definition_length: config_opts["max_definition_length"],
146
- }.merge(@options.slice(:include_diagrams, :format_definitions))
167
+ }.merge(@options.slice(:include_diagrams, :format_definitions,
168
+ :render_diagrams))
147
169
  end
148
170
 
149
171
  def search_options
@@ -163,12 +185,9 @@ module Lutaml
163
185
  end
164
186
 
165
187
  def setup_liquid
166
- # Use environment instead of deprecated class-level setters
167
- environment = Liquid::Environment.new
168
- environment.file_system = Liquid::LocalFileSystem.new(@options[:template_path])
169
- # Changed from :strict to handle missing includes
170
- environment.error_mode = :lax
171
- @liquid_environment = environment
188
+ @liquid_environment = Liquid::Environment.new
189
+ @liquid_environment.file_system = TemplateFileSystem.new(@options[:template_path])
190
+ @liquid_environment.error_mode = :lax
172
191
  end
173
192
 
174
193
  # Generate single-file SPA
@@ -226,7 +245,7 @@ module Lutaml
226
245
 
227
246
  def render_components # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
228
247
  # Set up Liquid file system for recursive includes
229
- temp_file_system = Liquid::LocalFileSystem.new(@options[:template_path])
248
+ temp_file_system = TemplateFileSystem.new(@options[:template_path])
230
249
 
231
250
  component_names = ["header", "sidebar", "content", "package_details",
232
251
  "class_details"]
@@ -274,7 +293,10 @@ module Lutaml
274
293
  # Render and write index.html
275
294
  template_content = File.read(File.join(@options[:template_path],
276
295
  "multi_file.liquid"))
277
- template = Liquid::Template.parse(template_content)
296
+ file_system = TemplateFileSystem.new(@options[:template_path])
297
+ template = Liquid::Template.parse(template_content,
298
+ error_mode: :lax)
299
+ template.registers[:file_system] = file_system
278
300
  html = template.render(context)
279
301
  html = minify_html(html) if @options[:minify]
280
302
  File.write(File.join(output_dir, "index.html"), html)
@@ -337,6 +359,7 @@ module Lutaml
337
359
  "03-layout.css",
338
360
  "04-components.css",
339
361
  "05-utilities.css",
362
+ "06-diagrams.css",
340
363
  ]
341
364
 
342
365
  css_parts = css_files.map do |file|
@@ -9,7 +9,8 @@ module Lutaml
9
9
  class ClassSerializer
10
10
  include Lutaml::Uml::ModelHelpers
11
11
 
12
- def initialize(repository, id_generator, options, inheritance_resolver)
12
+ def initialize(repository, id_generator, options,
13
+ inheritance_resolver)
13
14
  @repository = repository
14
15
  @id_generator = id_generator
15
16
  @options = options
@@ -81,6 +82,7 @@ module Lutaml
81
82
 
82
83
  def serialize_class_operations(klass)
83
84
  return [] unless klass.respond_to?(:operations) && klass.operations
85
+
84
86
  klass.operations.map { |op| @id_generator.operation_id(op, klass) }
85
87
  end
86
88
 
@@ -88,7 +90,8 @@ module Lutaml
88
90
  return [] unless klass.is_a?(Lutaml::Uml::Enum) && klass.owned_literal
89
91
 
90
92
  klass.owned_literal.map do |literal|
91
- { name: literal.name, definition: format_definition(literal.definition) }
93
+ { name: literal.name,
94
+ definition: format_definition(literal.definition) }
92
95
  end
93
96
  rescue StandardError
94
97
  []
@@ -97,11 +100,13 @@ module Lutaml
97
100
  def package_id_for_class(klass)
98
101
  ns = klass.respond_to?(:namespace) ? klass.namespace : nil
99
102
  return nil unless ns.is_a?(Lutaml::Uml::Package)
103
+
100
104
  @id_generator.package_id(ns)
101
105
  end
102
106
 
103
107
  def format_definition(definition)
104
108
  return nil if definition.nil? || definition.empty?
109
+
105
110
  formatted = definition.strip
106
111
  if @options[:max_definition_length] &&
107
112
  formatted.length > @options[:max_definition_length]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../presenters/diagram_presenter"
4
+
3
5
  module Lutaml
4
6
  module UmlRepository
5
7
  module StaticSite
@@ -25,13 +27,31 @@ module Lutaml
25
27
  private
26
28
 
27
29
  def serialize(diagram, id)
28
- {
30
+ data = {
29
31
  id: id,
30
32
  xmiId: diagram.xmi_id,
31
33
  name: diagram.name,
32
34
  type: diagram.diagram_type,
33
35
  package: find_diagram_package(diagram),
36
+ objectCount: (diagram.diagram_objects || []).size,
37
+ linkCount: (diagram.diagram_links || []).size,
34
38
  }
39
+
40
+ if @options[:render_diagrams]
41
+ svg = render_svg(diagram)
42
+ data[:svg] = svg if svg
43
+ end
44
+
45
+ data
46
+ end
47
+
48
+ def render_svg(diagram)
49
+ return nil unless diagram.diagram_objects&.any?
50
+
51
+ presenter = Presenters::DiagramPresenter.new(diagram, @repository)
52
+ presenter.svg_output
53
+ rescue StandardError
54
+ nil
35
55
  end
36
56
 
37
57
  def find_diagram_package(diagram)
@@ -48,6 +68,7 @@ module Lutaml
48
68
 
49
69
  def package_diagrams(package)
50
70
  return [] unless @options[:include_diagrams]
71
+
51
72
  package.diagrams || []
52
73
  rescue StandardError
53
74
  []
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require_relative "../../../uml/model_helpers"
5
4
  require_relative "../../class_lookup_index"
6
5
 
@@ -24,6 +23,7 @@ module Lutaml
24
23
  if parent_xmi_ids && !parent_xmi_ids.empty?
25
24
  parents = parent_xmi_ids.filter_map do |parent_xmi_id|
26
25
  next if parent_xmi_id == klass.xmi_id
26
+
27
27
  parent = class_lookup.by_xmi_id(parent_xmi_id)
28
28
  parent ? @id_generator.class_id(parent) : nil
29
29
  end
@@ -32,6 +32,7 @@ module Lutaml
32
32
 
33
33
  parent = @repository.supertype_of(klass)
34
34
  return [] if parent && parent.xmi_id == klass.xmi_id
35
+
35
36
  parent ? [@id_generator.class_id(parent)] : []
36
37
  rescue StandardError => e
37
38
  warn "Error finding generalizations for #{klass.name}: #{e.message}"
@@ -65,7 +66,9 @@ module Lutaml
65
66
  visited.add(parent_class.xmi_id)
66
67
 
67
68
  if parent_class.attributes
68
- sorted_attrs = parent_class.attributes.sort_by { |a| a.name || "" }
69
+ sorted_attrs = parent_class.attributes.sort_by do |a|
70
+ a.name || ""
71
+ end
69
72
  sorted_attrs.each do |attr|
70
73
  attr_id = @id_generator.attribute_id(attr, parent_class)
71
74
  inherited << {
@@ -164,7 +167,8 @@ module Lutaml
164
167
  ownedProps: serialize_general_attrs(gen, :owned_props),
165
168
  assocProps: serialize_general_attrs(gen, :assoc_props),
166
169
  inheritedProps: serialize_general_attrs(gen, :inherited_props),
167
- inheritedAssocProps: serialize_general_attrs(gen, :inherited_assoc_props),
170
+ inheritedAssocProps: serialize_general_attrs(gen,
171
+ :inherited_assoc_props),
168
172
  }
169
173
  rescue StandardError => e
170
174
  warn "Error serializing generalization: #{e.message}"
@@ -219,7 +223,10 @@ module Lutaml
219
223
 
220
224
  def serialize_general_attrs(gen, method)
221
225
  return [] unless gen.respond_to?(method)
222
- (gen.send(method) || []).map { |attr| serialize_general_attribute(attr) }
226
+
227
+ (gen.send(method) || []).map do |attr|
228
+ serialize_general_attribute(attr)
229
+ end
223
230
  end
224
231
 
225
232
  def serialize_cardinality(cardinality)
@@ -233,6 +240,7 @@ module Lutaml
233
240
 
234
241
  def format_definition(definition)
235
242
  return nil if definition.nil? || definition.empty?
243
+
236
244
  formatted = definition.strip
237
245
  if @options[:max_definition_length] &&
238
246
  formatted.length > @options[:max_definition_length]
@@ -38,9 +38,15 @@ module Lutaml
38
38
  stereotypes: normalize_stereotypes(
39
39
  package.respond_to?(:stereotype) ? package.stereotype : nil,
40
40
  ),
41
- classes: (package.classes || []).map { |c| @id_generator.class_id(c) },
42
- subPackages: (package.packages || []).map { |p| @id_generator.package_id(p) },
43
- diagrams: package_diagrams(package).map { |d| @id_generator.diagram_id(d) },
41
+ classes: (package.classes || []).map do |c|
42
+ @id_generator.class_id(c)
43
+ end,
44
+ subPackages: (package.packages || []).map do |p|
45
+ @id_generator.package_id(p)
46
+ end,
47
+ diagrams: package_diagrams(package).map do |d|
48
+ @id_generator.diagram_id(d)
49
+ end,
44
50
  parent: if package.respond_to?(:namespace) &&
45
51
  package.namespace.is_a?(Lutaml::Uml::Package)
46
52
  @id_generator.package_id(package.namespace)
@@ -59,6 +65,7 @@ module Lutaml
59
65
 
60
66
  def package_diagrams(package)
61
67
  return [] unless @options[:include_diagrams]
68
+
62
69
  package.diagrams || []
63
70
  rescue StandardError => e
64
71
  warn "Error getting diagrams for #{package.name}: #{e.message}"
@@ -44,14 +44,18 @@ module Lutaml
44
44
  def build_tree_node(package) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
45
45
  pkg_id = @id_generator.package_id(package)
46
46
 
47
- sorted_children = (package.packages || []).sort_by { |p| p.name || "" }
47
+ sorted_children = (package.packages || []).sort_by do |p|
48
+ p.name || ""
49
+ end
48
50
  sorted_classes = (package.classes || [])
49
51
  .reject { |c| c.name.nil? || c.name.empty? }
50
52
  .sort_by(&:name)
51
53
 
52
54
  child_nodes = sorted_children.map { |child| build_tree_node(child) }
53
55
 
54
- total_class_count = sorted_classes.size + child_nodes.sum { |c| c[:classCount] || 0 }
56
+ total_class_count = sorted_classes.size + child_nodes.sum do |c|
57
+ c[:classCount] || 0
58
+ end
55
59
 
56
60
  {
57
61
  id: pkg_id,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lutaml
4
- VERSION = "0.10.5"
4
+ VERSION = "0.10.7"
5
5
  end
@@ -10,9 +10,10 @@ document.addEventListener('alpine:init', () => {
10
10
  config: window.APP_CONFIG || {},
11
11
 
12
12
  // Current State
13
- currentView: 'welcome', // 'welcome' | 'package' | 'class' | 'search'
13
+ currentView: 'welcome', // 'welcome' | 'package' | 'class' | 'search' | 'diagram'
14
14
  currentPackage: null,
15
15
  currentClass: null,
16
+ currentDiagram: null,
16
17
  searchQuery: '',
17
18
  searchResults: [],
18
19
 
@@ -86,6 +87,7 @@ document.addEventListener('alpine:init', () => {
86
87
  this.currentView = 'welcome';
87
88
  this.currentPackage = null;
88
89
  this.currentClass = null;
90
+ this.currentDiagram = null;
89
91
  this.breadcrumbs = [];
90
92
  this.pushState();
91
93
  },
@@ -118,6 +120,21 @@ document.addEventListener('alpine:init', () => {
118
120
  this.pushState();
119
121
  },
120
122
 
123
+ selectDiagram(diagramId) {
124
+ if (!this.data || !this.data.diagrams[diagramId]) {
125
+ console.warn('Diagram not found:', diagramId);
126
+ return;
127
+ }
128
+
129
+ const diag = this.data.diagrams[diagramId];
130
+
131
+ this.currentDiagram = diagramId;
132
+ this.currentPackage = diag.package;
133
+ this.currentView = 'diagram';
134
+ this.updateBreadcrumbs();
135
+ this.pushState();
136
+ },
137
+
121
138
  performSearch(query) {
122
139
  if (!query || query.length < 2) {
123
140
  this.searchResults = [];
@@ -196,6 +213,8 @@ document.addEventListener('alpine:init', () => {
196
213
  this.breadcrumbs = this.buildPackageBreadcrumbs(this.currentPackage);
197
214
  } else if (this.currentView === 'class' && this.currentClass) {
198
215
  this.breadcrumbs = this.buildClassBreadcrumbs(this.currentClass);
216
+ } else if (this.currentView === 'diagram' && this.currentDiagram) {
217
+ this.breadcrumbs = [{ name: `Diagram: ${this.data.diagrams[this.currentDiagram]?.name || ''}`, type: 'diagram' }];
199
218
  } else if (this.currentView === 'search') {
200
219
  this.breadcrumbs = [{ name: `Search: ${this.searchQuery}`, type: 'search' }];
201
220
  }
@@ -275,6 +294,8 @@ document.addEventListener('alpine:init', () => {
275
294
  this.selectPackage(decodeURIComponent(parts[1]));
276
295
  } else if (parts[0] === 'class' && parts[1]) {
277
296
  this.selectClass(decodeURIComponent(parts[1]));
297
+ } else if (parts[0] === 'diagram' && parts[1]) {
298
+ this.selectDiagram(decodeURIComponent(parts[1]));
278
299
  } else if (parts[0] === 'search') {
279
300
  const params = new URLSearchParams(queryString);
280
301
  const query = params.get('q');
@@ -294,6 +315,8 @@ document.addEventListener('alpine:init', () => {
294
315
  hash = `#/package/${encodeURIComponent(this.currentPackage)}`;
295
316
  } else if (this.currentView === 'class' && this.currentClass) {
296
317
  hash = `#/class/${encodeURIComponent(this.currentClass)}`;
318
+ } else if (this.currentView === 'diagram' && this.currentDiagram) {
319
+ hash = `#/diagram/${encodeURIComponent(this.currentDiagram)}`;
297
320
  } else if (this.currentView === 'search' && this.searchQuery) {
298
321
  hash = `#/search?q=${encodeURIComponent(this.searchQuery)}`;
299
322
  }
@@ -370,6 +393,7 @@ function app() {
370
393
  get currentView() { return Alpine.store('app').currentView; },
371
394
  get currentPackage() { return Alpine.store('app').currentPackage; },
372
395
  get currentClass() { return Alpine.store('app').currentClass; },
396
+ get currentDiagram() { return Alpine.store('app').currentDiagram; },
373
397
  get sidebarVisible() {
374
398
  return Alpine.store('app').sidebarVisible;
375
399
  },
@@ -392,6 +416,9 @@ function app() {
392
416
  selectClass(id) {
393
417
  Alpine.store('app').selectClass(id);
394
418
  },
419
+ selectDiagram(id) {
420
+ Alpine.store('app').selectDiagram(id);
421
+ },
395
422
  toggleTheme() {
396
423
  Alpine.store('app').toggleTheme();
397
424
  },
@@ -274,4 +274,52 @@
274
274
  .diagram-wrapper svg {
275
275
  filter: brightness(0.9);
276
276
  }
277
+ }
278
+
279
+ /* Inline Diagram Viewer (content pane) */
280
+ .diagram-toolbar {
281
+ display: flex;
282
+ gap: 0.5rem;
283
+ padding: 0.75rem 0;
284
+ margin-bottom: 1rem;
285
+ }
286
+
287
+ .diagram-toolbar .btn-sm {
288
+ padding: 0.375rem 0.75rem;
289
+ font-size: 0.8rem;
290
+ background: var(--bg-secondary, #f8f9fa);
291
+ border: 1px solid var(--color-border, #dcdfe6);
292
+ border-radius: 4px;
293
+ cursor: pointer;
294
+ transition: all 0.15s ease;
295
+ }
296
+
297
+ .diagram-toolbar .btn-sm:hover {
298
+ background: var(--color-primary, #3498db);
299
+ color: white;
300
+ border-color: var(--color-primary, #3498db);
301
+ }
302
+
303
+ .diagram-svg-container {
304
+ background: white;
305
+ border: 1px solid var(--color-border, #dcdfe6);
306
+ border-radius: 8px;
307
+ padding: 1rem;
308
+ overflow: hidden;
309
+ transition: transform 0.1s ease-out;
310
+ min-height: 400px;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ }
315
+
316
+ .diagram-svg-container svg {
317
+ max-width: 100%;
318
+ display: block;
319
+ }
320
+
321
+ .diagram-item .diagram-objects {
322
+ font-size: 0.8rem;
323
+ color: var(--color-text-secondary, #7f8c8d);
324
+ margin-left: auto;
277
325
  }
@@ -73,7 +73,7 @@
73
73
  <h3>Diagrams (<span x-text="pkg.diagrams.length"></span>)</h3>
74
74
  <ul class="diagram-list">
75
75
  <template x-for="diagramId in pkg.diagrams" :key="diagramId">
76
- <li class="diagram-item">
76
+ <li class="diagram-item clickable-row" @click="$store.app.selectDiagram(diagramId)">
77
77
  <svg class="item-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
78
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
79
79
  <line x1="3" y1="9" x2="21" y2="9"></line>
@@ -81,6 +81,7 @@
81
81
  </svg>
82
82
  <span class="diagram-name" x-text="$store.app.data.diagrams[diagramId] && $store.app.data.diagrams[diagramId].name"></span>
83
83
  <span class="diagram-type" x-text="$store.app.data.diagrams[diagramId] && `(${$store.app.data.diagrams[diagramId].type})`"></span>
84
+ <span class="diagram-objects" x-show="$store.app.data.diagrams[diagramId] && $store.app.data.diagrams[diagramId].objectCount" x-text="`${$store.app.data.diagrams[diagramId].objectCount} objects`"></span>
84
85
  </li>
85
86
  </template>
86
87
  </ul>
@@ -503,6 +504,75 @@
503
504
  </template>
504
505
  </div>
505
506
  </div>
507
+
508
+ <!-- Diagram View -->
509
+ <div x-show="currentView === 'diagram'" x-transition class="view-diagram">
510
+ <template x-if="currentDiagram && data.diagrams[currentDiagram]">
511
+ <div class="diagram-details" x-data="{
512
+ get diag() { return data.diagrams[currentDiagram]; },
513
+ get pkg() { return diag.package ? data.packages[diag.package] : null; },
514
+ diagramScale: 1.0,
515
+ diagramPanX: 0,
516
+ diagramPanY: 0,
517
+ isPanning: false,
518
+ panStartX: 0,
519
+ panStartY: 0,
520
+ zoom(factor) { this.diagramScale = Math.max(0.1, Math.min(5.0, this.diagramScale * factor)); },
521
+ resetView() { this.diagramScale = 1.0; this.diagramPanX = 0; this.diagramPanY = 0; },
522
+ startPan(e) { this.isPanning = true; this.panStartX = e.clientX - this.diagramPanX; this.panStartY = e.clientY - this.diagramPanY; },
523
+ doPan(e) { if (!this.isPanning) return; this.diagramPanX = e.clientX - this.panStartX; this.diagramPanY = e.clientY - this.panStartY; },
524
+ endPan() { this.isPanning = false; },
525
+ downloadSvg() {
526
+ const svgEl = document.querySelector('.diagram-svg-container svg');
527
+ if (!svgEl) return;
528
+ const data = new XMLSerializer().serializeToString(svgEl);
529
+ const blob = new Blob([data], { type: 'image/svg+xml' });
530
+ const url = URL.createObjectURL(blob);
531
+ const a = document.createElement('a');
532
+ a.href = url; a.download = (this.diag.name || 'diagram').replace(/[^a-zA-Z0-9_-]/g, '_') + '.svg';
533
+ a.click(); URL.revokeObjectURL(url);
534
+ }
535
+ }">
536
+ <div class="entity-header">
537
+ <div class="entity-title">
538
+ <h1 class="entity-name" x-text="diag.name"></h1>
539
+ <p class="entity-subtitle">
540
+ <span x-text="diag.type"></span>
541
+ <span x-show="diag.objectCount"> - <span x-text="diag.objectCount"></span> elements, <span x-text="diag.linkCount"></span> connectors</span>
542
+ </p>
543
+ </div>
544
+ <div style="display: flex; gap: 0.5rem; align-items: center;">
545
+ <button class="link-button" x-show="diag.package" @click="selectPackage(diag.package)">
546
+ &larr; Back to package
547
+ </button>
548
+ <span class="entity-type-badge">Diagram</span>
549
+ </div>
550
+ </div>
551
+
552
+ <div class="diagram-toolbar" x-show="diag.svg">
553
+ <button class="btn btn-sm" @click="zoom(1.2)">Zoom In</button>
554
+ <button class="btn btn-sm" @click="zoom(0.8)">Zoom Out</button>
555
+ <button class="btn btn-sm" @click="resetView()">Reset</button>
556
+ <button class="btn btn-sm" @click="downloadSvg()">Download SVG</button>
557
+ </div>
558
+
559
+ <div class="diagram-svg-container"
560
+ x-show="diag.svg"
561
+ x-ref="diagramContainer"
562
+ @wheel.prevent="zoom($event.deltaY > 0 ? 0.9 : 1.1)"
563
+ @mousedown="startPan($event)"
564
+ @mousemove="doPan($event)"
565
+ @mouseup="endPan()"
566
+ @mouseleave="endPan()"
567
+ :style="`transform: translate(${diagramPanX}px, ${diagramPanY}px) scale(${diagramScale}); transform-origin: center; cursor: ${isPanning ? 'grabbing' : 'grab'};`">
568
+ <div x-html="diag.svg"></div>
569
+ </div>
570
+
571
+ <div x-show="!diag.svg" class="empty-state">
572
+ <p class="empty-state-message">SVG rendering not available for this diagram. Re-generate with <code>--render-diagrams</code> to include SVG.</p>
573
+ </div>
574
+ </div>
575
+ </template>
506
576
  </div>
507
- </div>
508
- </main>
577
+
578
+ </div>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.5
4
+ version: 0.10.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-28 00:00:00.000000000 Z
11
+ date: 2026-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir