xsdvi 1.0.0 → 1.0.2

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.
@@ -29,7 +29,7 @@ module Xsdvi
29
29
 
30
30
  def draw
31
31
  print("<a href=\"#\" onclick=\"window.parent.location.href = " \
32
- "window.parent.location.href.split('#')[0] + " \
32
+ "window.parent.location.href.split('#')[0] + " \
33
33
  "'#element_#{name}'\">")
34
34
 
35
35
  process_description
data/lib/xsdvi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xsdvi
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri"
4
+ require "set"
4
5
 
5
6
  module Xsdvi
6
7
  # Handles XSD parsing and tree building
@@ -16,6 +17,17 @@ module Xsdvi
16
17
  @root_node_name = nil
17
18
  @one_node_only = false
18
19
  @schema_namespace = nil
20
+
21
+ # Type registries for resolution (Phase 1)
22
+ @complex_types = {} # QName => complexType node
23
+ @simple_types = {} # QName => simpleType node
24
+ @model_groups = {} # QName => group node
25
+ @attribute_groups = {} # QName => attributeGroup node
26
+ @elements = {} # QName => element node (for refs)
27
+
28
+ # Type extension/restriction chain tracking (prevents stack overflow)
29
+ @type_depth = 0
30
+ @type_ancestors = Set.new
19
31
  end
20
32
 
21
33
  def process_file(file_path)
@@ -30,12 +42,19 @@ module Xsdvi
30
42
  schema_node = doc.at_xpath("/xs:schema", "xs" => XSD_NAMESPACE)
31
43
  @schema_namespace = schema_node["targetNamespace"] if schema_node
32
44
 
45
+ # Phase 1: Collect all type definitions, groups, and elements
46
+ collect_type_definitions(doc)
47
+ collect_group_definitions(doc)
48
+ collect_attribute_group_definitions(doc)
49
+ collect_element_definitions(doc)
50
+
33
51
  # Create schema symbol when no root node specified (matches Java line 65-68)
34
52
  if root_node_name.nil?
35
53
  symbol = SVG::Symbols::Schema.new
36
54
  builder.set_root(symbol)
37
55
  end
38
56
 
57
+ # Phase 2: Process elements with type resolution
39
58
  process_element_declarations(doc)
40
59
 
41
60
  # Level up after processing all elements (matches Java line 71-73)
@@ -61,6 +80,42 @@ module Xsdvi
61
80
 
62
81
  private
63
82
 
83
+ def collect_type_definitions(doc)
84
+ # Collect complexType definitions
85
+ doc.xpath("//xs:complexType[@name]", "xs" => XSD_NAMESPACE).each do |node|
86
+ name = node["name"]
87
+ @complex_types[name] = node
88
+ end
89
+
90
+ # Collect simpleType definitions
91
+ doc.xpath("//xs:simpleType[@name]", "xs" => XSD_NAMESPACE).each do |node|
92
+ name = node["name"]
93
+ @simple_types[name] = node
94
+ end
95
+ end
96
+
97
+ def collect_group_definitions(doc)
98
+ doc.xpath("//xs:group[@name]", "xs" => XSD_NAMESPACE).each do |node|
99
+ name = node["name"]
100
+ @model_groups[name] = node
101
+ end
102
+ end
103
+
104
+ def collect_attribute_group_definitions(doc)
105
+ doc.xpath("//xs:attributeGroup[@name]",
106
+ "xs" => XSD_NAMESPACE).each do |node|
107
+ name = node["name"]
108
+ @attribute_groups[name] = node
109
+ end
110
+ end
111
+
112
+ def collect_element_definitions(doc)
113
+ doc.xpath("//xs:element[@name]", "xs" => XSD_NAMESPACE).each do |node|
114
+ name = node["name"]
115
+ @elements[name] = node
116
+ end
117
+ end
118
+
64
119
  def process_element_declarations(doc)
65
120
  elements = doc.xpath("//xs:schema/xs:element", "xs" => XSD_NAMESPACE)
66
121
 
@@ -75,9 +130,14 @@ module Xsdvi
75
130
  end
76
131
 
77
132
  def process_element_declaration(elem_node, cardinality, is_root)
133
+ # Phase 5: Handle element references first
134
+ if (ref = elem_node["ref"])
135
+ process_element_ref(ref, cardinality)
136
+ return
137
+ end
138
+
78
139
  name = elem_node["name"]
79
140
  elem_namespace = elem_node["namespace"]
80
- elem_node["ref"]
81
141
  type_attr = elem_node["type"]
82
142
  nillable = elem_node["nillable"] == "true"
83
143
  abstract = elem_node["abstract"] == "true"
@@ -118,20 +178,37 @@ module Xsdvi
118
178
 
119
179
  # Skip processing children if stack size > 1 and oneNodeOnly (matches Java line 253-260)
120
180
  unless @stack.size > 1 && one_node_only
121
- # Check if element has inline complexType
181
+ # Check for inline complexType first
122
182
  complex_type = elem_node.at_xpath("xs:complexType",
123
183
  "xs" => XSD_NAMESPACE)
124
184
  if complex_type
185
+ # Process inline anonymous complexType
125
186
  process_complex_type_node(complex_type)
126
- elsif !type_attr
187
+ elsif type_attr
188
+ # Try to resolve type reference
189
+ resolved_type = resolve_type(type_attr)
190
+ if resolved_type
191
+ # Process the resolved named type
192
+ if resolved_type.name == "complexType"
193
+ process_complex_type_node(resolved_type)
194
+ elsif resolved_type.name == "simpleType"
195
+ # simpleTypes don't have children to process
196
+ # The type string is already set above
197
+ end
198
+ elsif type_attr == "anyType"
199
+ # Explicit anyType reference
200
+ process_any_type_default
201
+ end
202
+ # If type is built-in (string, int, etc), we just show the type string
203
+ else
127
204
  # No type means anyType - expand it
128
205
  process_any_type_default
129
- elsif type_attr == "anyType"
130
- process_any_type_default
131
206
  end
132
207
  end
133
208
 
134
209
  @stack.pop
210
+ # Process identity constraints (matches Java line 262)
211
+ process_identity_constraints(elem_node)
135
212
  builder.level_up
136
213
  end
137
214
 
@@ -177,25 +254,108 @@ module Xsdvi
177
254
  end
178
255
 
179
256
  def process_complex_type_node(complex_type_node)
257
+ # Phase 6: Check for complexContent or simpleContent first
258
+ complex_content = complex_type_node.at_xpath("xs:complexContent",
259
+ "xs" => XSD_NAMESPACE)
260
+ simple_content = complex_type_node.at_xpath("xs:simpleContent",
261
+ "xs" => XSD_NAMESPACE)
262
+
263
+ if complex_content
264
+ process_complex_content(complex_content)
265
+ elsif simple_content
266
+ process_simple_content(simple_content)
267
+ else
268
+ # Direct content model (no extension/restriction wrapper)
269
+ process_complex_type_content(complex_type_node)
270
+ end
271
+ end
272
+
273
+ # Phase 6: Process complexContent
274
+ def process_complex_content(complex_content_node)
275
+ extension = complex_content_node.at_xpath("xs:extension",
276
+ "xs" => XSD_NAMESPACE)
277
+ restriction = complex_content_node.at_xpath("xs:restriction",
278
+ "xs" => XSD_NAMESPACE)
279
+
280
+ if extension
281
+ process_extension(extension)
282
+ elsif restriction
283
+ process_restriction(restriction)
284
+ end
285
+ end
286
+
287
+ # Phase 6: Process simpleContent
288
+ def process_simple_content(simple_content_node)
289
+ extension = simple_content_node.at_xpath("xs:extension",
290
+ "xs" => XSD_NAMESPACE)
291
+ restriction = simple_content_node.at_xpath("xs:restriction",
292
+ "xs" => XSD_NAMESPACE)
293
+
294
+ if extension
295
+ process_extension(extension)
296
+ elsif restriction
297
+ process_restriction(restriction)
298
+ end
299
+ end
300
+
301
+ # Phase 6: Process extension
302
+ def process_extension(extension_node)
303
+ # Process base type first (inherit content model)
304
+ if (base = extension_node["base"])
305
+ base_type = resolve_type(base)
306
+ if base_type && base_type.name == "complexType" && type_chain_enter(base)
307
+ process_complex_type_node(base_type)
308
+ type_chain_exit(base)
309
+ end
310
+ end
311
+
312
+ # Then process extension's own content
313
+ process_complex_type_content(extension_node)
314
+ end
315
+
316
+ # Phase 6: Process restriction
317
+ def process_restriction(restriction_node)
318
+ # Process base type first (inherit and potentially restrict content model)
319
+ if (base = restriction_node["base"])
320
+ base_type = resolve_type(base)
321
+ if base_type && base_type.name == "complexType" && type_chain_enter(base)
322
+ process_complex_type_node(base_type)
323
+ type_chain_exit(base)
324
+ end
325
+ end
326
+
327
+ # Then process restriction's own content
328
+ process_complex_type_content(restriction_node)
329
+ end
330
+
331
+ # Phase 6: Process complex type content (particles and attributes)
332
+ def process_complex_type_content(node)
180
333
  # Process particles (sequence/choice/all)
181
- sequence = complex_type_node.at_xpath("xs:sequence",
182
- "xs" => XSD_NAMESPACE)
334
+ sequence = node.at_xpath("xs:sequence", "xs" => XSD_NAMESPACE)
183
335
  process_sequence(sequence, nil) if sequence
184
336
 
185
- choice = complex_type_node.at_xpath("xs:choice", "xs" => XSD_NAMESPACE)
337
+ choice = node.at_xpath("xs:choice", "xs" => XSD_NAMESPACE)
186
338
  process_choice(choice, nil) if choice
187
339
 
188
- all_node = complex_type_node.at_xpath("xs:all", "xs" => XSD_NAMESPACE)
340
+ all_node = node.at_xpath("xs:all", "xs" => XSD_NAMESPACE)
189
341
  process_all(all_node, nil) if all_node
190
342
 
191
- # Process attributes
192
- attributes = complex_type_node.xpath("xs:attribute",
193
- "xs" => XSD_NAMESPACE)
194
- attributes.each { |attr| process_attribute(attr) }
343
+ # Process attributes (both direct and references) - SORT ALPHABETICALLY
344
+ attributes = node.xpath("xs:attribute", "xs" => XSD_NAMESPACE)
345
+ sorted_attributes = attributes.sort_by do |attr|
346
+ # Sort by ref or name
347
+ attr["ref"] || attr["name"] || ""
348
+ end
349
+ sorted_attributes.each { |attr| process_attribute(attr) }
350
+
351
+ # Process attribute group references
352
+ attr_groups = node.xpath("xs:attributeGroup[@ref]", "xs" => XSD_NAMESPACE)
353
+ attr_groups.each do |attr_group|
354
+ process_attribute_group_ref(attr_group["ref"])
355
+ end
195
356
 
196
357
  # Process attribute wildcard
197
- any_attr = complex_type_node.at_xpath("xs:anyAttribute",
198
- "xs" => XSD_NAMESPACE)
358
+ any_attr = node.at_xpath("xs:anyAttribute", "xs" => XSD_NAMESPACE)
199
359
  process_any_attribute(any_attr) if any_attr
200
360
  end
201
361
 
@@ -211,6 +371,12 @@ module Xsdvi
211
371
  process_element_declaration(elem, get_cardinality(elem), false)
212
372
  end
213
373
 
374
+ # Process group references (Phase 3)
375
+ sequence_node.xpath("xs:group[@ref]",
376
+ "xs" => XSD_NAMESPACE).each do |group|
377
+ process_group_ref(group["ref"])
378
+ end
379
+
214
380
  # Process any
215
381
  sequence_node.xpath("xs:any", "xs" => XSD_NAMESPACE).each do |any|
216
382
  process_any(any)
@@ -231,6 +397,11 @@ module Xsdvi
231
397
  process_element_declaration(elem, get_cardinality(elem), false)
232
398
  end
233
399
 
400
+ # Process group references (Phase 3)
401
+ choice_node.xpath("xs:group[@ref]", "xs" => XSD_NAMESPACE).each do |group|
402
+ process_group_ref(group["ref"])
403
+ end
404
+
234
405
  builder.level_up
235
406
  end
236
407
 
@@ -246,6 +417,11 @@ module Xsdvi
246
417
  process_element_declaration(elem, get_cardinality(elem), false)
247
418
  end
248
419
 
420
+ # Process group references (Phase 3)
421
+ all_node.xpath("xs:group[@ref]", "xs" => XSD_NAMESPACE).each do |group|
422
+ process_group_ref(group["ref"])
423
+ end
424
+
249
425
  builder.level_up
250
426
  end
251
427
 
@@ -258,7 +434,7 @@ module Xsdvi
258
434
  when "skip" then SVG::Symbol::PC_SKIP
259
435
  when "lax" then SVG::Symbol::PC_LAX
260
436
  when "strict" then SVG::Symbol::PC_STRICT
261
- else SVG::Symbol::PC_LAX # Default is lax for anyType
437
+ else SVG::Symbol::PC_LAX # Default is lax for anyType
262
438
  end
263
439
  symbol.cardinality = get_cardinality(any_node)
264
440
  symbol.description = extract_documentation(any_node)
@@ -267,18 +443,123 @@ module Xsdvi
267
443
  end
268
444
 
269
445
  def process_attribute(attr_node)
446
+ # Handle attribute references (like <xsd:attribute ref="xml:lang"/>)
447
+ if (ref = attr_node["ref"])
448
+ process_attribute_ref(ref, attr_node)
449
+ return
450
+ end
451
+
270
452
  symbol = SVG::Symbols::Attribute.new
271
453
  symbol.name = attr_node["name"]
272
454
  namespace = attr_node["namespace"]
273
455
  symbol.namespace = namespace if namespace && namespace != schema_namespace
274
- symbol.type = attr_node["type"] ? "type: #{attr_node['type']}" : nil
456
+
457
+ # Determine type - either from type attribute or inline simpleType restriction
458
+ type_value = nil
459
+ type_prefix = "type"
460
+
461
+ if attr_node["type"]
462
+ type_value = attr_node["type"]
463
+ if type_value.start_with?("xsd:")
464
+ type_value = type_value.sub(/^xsd:/,
465
+ "")
466
+ end
467
+ else
468
+ # Check for inline simpleType definition
469
+ simple_type = attr_node.at_xpath("xs:simpleType", "xs" => XSD_NAMESPACE)
470
+ if simple_type
471
+ # Check for union type first
472
+ union = simple_type.at_xpath("xs:union", "xs" => XSD_NAMESPACE)
473
+ if union
474
+ # Union types have base type of anySimpleType
475
+ type_value = "anySimpleType"
476
+ type_prefix = "base"
477
+ else
478
+ # Check for restriction
479
+ restriction = simple_type.at_xpath("xs:restriction",
480
+ "xs" => XSD_NAMESPACE)
481
+ if restriction && restriction["base"]
482
+ type_value = restriction["base"]
483
+ if type_value.start_with?("xsd:")
484
+ type_value = type_value.sub(/^xsd:/,
485
+ "")
486
+ end
487
+ type_prefix = "base"
488
+ end
489
+ end
490
+ end
491
+ end
492
+
493
+ symbol.type = "#{type_prefix}: #{type_value}" if type_value
494
+
275
495
  symbol.required = attr_node["use"] == "required"
496
+
497
+ # Capture default or fixed values
498
+ if attr_node["default"]
499
+ default_value = attr_node["default"]
500
+
501
+ # Format default value for double type: 0 becomes 0.0E1
502
+ if type_value == "double" && (default_value == "0" || default_value.to_f == 0.0)
503
+ default_value = "0.0E1"
504
+ end
505
+
506
+ symbol.constraint = "default: #{default_value}"
507
+ elsif attr_node["fixed"]
508
+ symbol.constraint = "fixed: #{attr_node['fixed']}"
509
+ end
510
+
276
511
  symbol.description = extract_documentation(attr_node)
277
512
 
278
513
  builder.append_child(symbol)
279
514
  builder.level_up
280
515
  end
281
516
 
517
+ # Process attribute reference
518
+ def process_attribute_ref(ref, ref_node)
519
+ # Extract namespace prefix and local name
520
+ if ref.include?(":")
521
+ prefix, local_name = ref.split(":", 2)
522
+ else
523
+ prefix = nil
524
+ local_name = ref
525
+ end
526
+
527
+ symbol = SVG::Symbols::Attribute.new
528
+
529
+ # Handle xml: namespace attributes specially
530
+ if prefix == "xml"
531
+ # W3C XML namespace
532
+ symbol.namespace = "http://www.w3.org/XML/1998/namespace"
533
+ symbol.name = local_name # Just "lang" or "id", not "xml:lang" or "xml:id"
534
+
535
+ # Set type based on specific xml: attribute
536
+ symbol.type = case local_name
537
+ when "id"
538
+ "type: ID"
539
+ when "lang"
540
+ "base: anySimpleType"
541
+ when "space"
542
+ "type: NCName"
543
+ when "base"
544
+ "type: anyURI"
545
+ else
546
+ "base: anySimpleType"
547
+ end
548
+ else
549
+ # Regular attribute reference - would need to look up in schema
550
+ symbol.name = local_name
551
+ end
552
+
553
+ # Get use constraint from the reference location
554
+ symbol.required = ref_node["use"] == "required"
555
+
556
+ # Extract documentation from the reference node itself
557
+ symbol.description = extract_documentation(ref_node)
558
+
559
+ builder.append_child(symbol)
560
+ builder.level_up
561
+ end
562
+
282
563
  def process_any_attribute(any_attr_node)
283
564
  symbol = SVG::Symbols::AnyAttribute.new
284
565
  namespace = any_attr_node["namespace"]
@@ -288,36 +569,217 @@ module Xsdvi
288
569
  when "skip" then SVG::Symbol::PC_SKIP
289
570
  when "lax" then SVG::Symbol::PC_LAX
290
571
  when "strict" then SVG::Symbol::PC_STRICT
291
- else SVG::Symbol::PC_LAX # Default is lax for anyType
572
+ else SVG::Symbol::PC_LAX # Default is lax for anyType
292
573
  end
293
574
  symbol.description = extract_documentation(any_attr_node)
294
575
  builder.append_child(symbol)
295
576
  builder.level_up
296
577
  end
297
578
 
579
+ # Phase 3: Group reference resolution
580
+ def process_group_ref(ref)
581
+ return unless ref
582
+
583
+ # Strip namespace prefix if present
584
+ group_name = ref.include?(":") ? ref.split(":").last : ref
585
+ group_node = @model_groups[group_name]
586
+
587
+ return unless group_node
588
+
589
+ # Process the referenced group's content (sequence, choice, or all)
590
+ model_group = group_node.at_xpath("xs:sequence | xs:choice | xs:all",
591
+ "xs" => XSD_NAMESPACE)
592
+ return unless model_group
593
+
594
+ case model_group.name
595
+ when "sequence"
596
+ process_sequence(model_group, nil)
597
+ when "choice"
598
+ process_choice(model_group, nil)
599
+ when "all"
600
+ process_all(model_group, nil)
601
+ end
602
+ end
603
+
604
+ # Phase 4: Attribute group reference resolution
605
+ def process_attribute_group_ref(ref)
606
+ return unless ref
607
+
608
+ # Strip namespace prefix if present
609
+ group_name = ref.include?(":") ? ref.split(":").last : ref
610
+ group_node = @attribute_groups[group_name]
611
+
612
+ return unless group_node
613
+
614
+ # Process all attributes in the group
615
+ group_node.xpath("xs:attribute", "xs" => XSD_NAMESPACE).each do |attr|
616
+ process_attribute(attr)
617
+ end
618
+
619
+ # Process nested attribute group references (can be recursive)
620
+ group_node.xpath("xs:attributeGroup[@ref]",
621
+ "xs" => XSD_NAMESPACE).each do |nested|
622
+ process_attribute_group_ref(nested["ref"])
623
+ end
624
+ end
625
+
626
+ # Phase 5: Element reference resolution
627
+ def process_element_ref(ref, cardinality)
628
+ return unless ref
629
+
630
+ # Strip namespace prefix if present
631
+ elem_name = ref.include?(":") ? ref.split(":").last : ref
632
+ elem_node = @elements[elem_name]
633
+
634
+ return unless elem_node
635
+
636
+ # Check for circular reference to prevent infinite recursion
637
+ if @stack.any? { |e| e["name"] == elem_name }
638
+ # Create a loop symbol instead (Loop doesn't have a name attribute)
639
+ symbol = SVG::Symbols::Loop.new
640
+ builder.append_child(symbol)
641
+ builder.level_up
642
+ return
643
+ end
644
+
645
+ # Process the referenced element
646
+ process_element_declaration(elem_node, cardinality, false)
647
+ end
648
+
298
649
  def process_loop?(elem_node)
299
650
  @stack.any?(elem_node)
300
651
  end
301
652
 
302
653
  def get_cardinality(node)
303
654
  min_occurs = node["minOccurs"]&.to_i || 1
304
- max_occurs = node["maxOccurs"]
655
+ max_occurs_str = node["maxOccurs"] || "1" # XSD default is "1" when not specified
305
656
 
306
- return nil if min_occurs == 1 && (max_occurs.nil? || max_occurs == "1")
657
+ return nil if min_occurs == 1 && max_occurs_str == "1"
307
658
 
308
- if max_occurs == "unbounded"
659
+ if max_occurs_str == "unbounded"
309
660
  "#{min_occurs}..∞"
310
- elsif max_occurs
661
+ else
662
+ max_occurs = max_occurs_str.to_i
311
663
  "#{min_occurs}..#{max_occurs}"
312
664
  end
313
665
  end
314
666
 
315
667
  def extract_documentation(node)
316
668
  docs = node.xpath(
317
- ".//xs:annotation/xs:documentation",
669
+ "./xs:annotation/xs:documentation", # Changed from .// to ./ (direct children only)
318
670
  "xs" => XSD_NAMESPACE,
319
671
  )
320
- docs.map(&:text).map { |text| text.gsub(/\n[ \t]+/, "\n") }
672
+ # Use inner_html to preserve XML entities like &lt; and &gt;
673
+ # Java's XML parser preserves these, affecting wrap length calculations
674
+ docs.map(&:inner_html).map { |text| text.gsub(/\n[ \t]+/, "\n") }
675
+ end
676
+
677
+ # Phase 2: Type resolution methods
678
+ def resolve_type(type_attr)
679
+ return nil unless type_attr
680
+
681
+ # Strip namespace prefix if present (e.g., "xs:string" -> "string")
682
+ type_name = type_attr.include?(":") ? type_attr.split(":").last : type_attr
683
+
684
+ # Don't try to resolve built-in XSD types
685
+ return nil if is_builtin_type?(type_name)
686
+
687
+ # Look up in registries
688
+ @complex_types[type_name] || @simple_types[type_name]
689
+ end
690
+
691
+ def is_builtin_type?(type_name)
692
+ # W3C XML Schema built-in types
693
+ %w[
694
+ string boolean decimal float double duration dateTime
695
+ time date gYearMonth gYear gMonthDay gDay gMonth
696
+ hexBinary base64Binary anyURI QName NOTATION
697
+ normalizedString token language NMTOKEN NMTOKENS
698
+ Name NCName ID IDREF IDREFS ENTITY ENTITIES
699
+ integer nonPositiveInteger negativeInteger long int
700
+ short byte nonNegativeInteger unsignedLong unsignedInt
701
+ unsignedShort unsignedByte positiveInteger
702
+ anyType anySimpleType anyAtomicType
703
+ ].include?(type_name)
704
+ end
705
+
706
+ def process_identity_constraints(elem_node)
707
+ # Process xs:key
708
+ elem_node.xpath("xs:key", "xs" => XSD_NAMESPACE).each do |constraint|
709
+ process_identity_constraint(constraint, :key)
710
+ end
711
+
712
+ # Process xs:keyref
713
+ elem_node.xpath("xs:keyref", "xs" => XSD_NAMESPACE).each do |constraint|
714
+ process_identity_constraint(constraint, :keyref)
715
+ end
716
+
717
+ # Process xs:unique
718
+ elem_node.xpath("xs:unique", "xs" => XSD_NAMESPACE).each do |constraint|
719
+ process_identity_constraint(constraint, :unique)
720
+ end
721
+ end
722
+
723
+ def process_identity_constraint(constraint_node, category)
724
+ # Create appropriate symbol based on category
725
+ symbol = case category
726
+ when :key
727
+ SVG::Symbols::Key.new
728
+ when :keyref
729
+ SVG::Symbols::Keyref.new
730
+ when :unique
731
+ SVG::Symbols::Unique.new
732
+ end
733
+
734
+ # Set common properties
735
+ symbol.name = constraint_node["name"]
736
+ namespace = constraint_node["namespace"]
737
+ symbol.namespace = namespace if namespace && namespace != schema_namespace
738
+ symbol.description = extract_documentation(constraint_node)
739
+
740
+ # For keyref, set the refer attribute
741
+ if category == :keyref
742
+ symbol.refer = constraint_node["refer"]
743
+ end
744
+
745
+ builder.append_child(symbol)
746
+
747
+ # Process selector
748
+ selector_node = constraint_node.at_xpath("xs:selector",
749
+ "xs" => XSD_NAMESPACE)
750
+ if selector_node
751
+ selector_symbol = SVG::Symbols::Selector.new
752
+ selector_symbol.xpath = selector_node["xpath"]
753
+ builder.append_child(selector_symbol)
754
+ builder.level_up
755
+ end
756
+
757
+ # Process field(s)
758
+ constraint_node.xpath("xs:field",
759
+ "xs" => XSD_NAMESPACE).each do |field_node|
760
+ field_symbol = SVG::Symbols::Field.new
761
+ field_symbol.xpath = field_node["xpath"]
762
+ builder.append_child(field_symbol)
763
+ builder.level_up
764
+ end
765
+
766
+ builder.level_up
767
+ end
768
+
769
+ MAX_TYPE_DEPTH = 100
770
+
771
+ def type_chain_enter(type_name)
772
+ return false if @type_depth >= MAX_TYPE_DEPTH
773
+ return false if @type_ancestors.include?(type_name)
774
+
775
+ @type_depth += 1
776
+ @type_ancestors.add(type_name)
777
+ true
778
+ end
779
+
780
+ def type_chain_exit(type_name)
781
+ @type_depth -= 1
782
+ @type_ancestors.delete(type_name)
321
783
  end
322
784
  end
323
785
  end
data/lib/xsdvi.rb CHANGED
@@ -25,6 +25,12 @@ require_relative "xsdvi/utils/writer"
25
25
  require_relative "xsdvi/utils/resource_loader"
26
26
  require_relative "xsdvi/utils/width_calculator"
27
27
 
28
+ # Comparison components
29
+ require_relative "xsdvi/comparison/java_manager"
30
+ require_relative "xsdvi/comparison/metadata_extractor"
31
+ require_relative "xsdvi/comparison/html_generator"
32
+ require_relative "xsdvi/comparison/dual_generator"
33
+
28
34
  module Xsdvi
29
35
  class Error < StandardError; end
30
36
  end