xseed 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +16 -0
  3. data/.github/workflows/release.yml +25 -0
  4. data/.gitignore +72 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +11 -0
  7. data/.rubocop_todo.yml +432 -0
  8. data/CHANGELOG.adoc +446 -0
  9. data/Gemfile +21 -0
  10. data/LICENSE.adoc +29 -0
  11. data/README.adoc +386 -0
  12. data/Rakefile +11 -0
  13. data/examples/README.adoc +334 -0
  14. data/examples/advanced_usage.rb +286 -0
  15. data/examples/html_generation.rb +167 -0
  16. data/examples/parser_usage.rb +102 -0
  17. data/examples/schemas/person.xsd +171 -0
  18. data/examples/simple_generation.rb +149 -0
  19. data/exe/xseed +6 -0
  20. data/lib/xseed/cli.rb +376 -0
  21. data/lib/xseed/documentation/config.rb +101 -0
  22. data/lib/xseed/documentation/constants.rb +76 -0
  23. data/lib/xseed/documentation/generators/hierarchy_table_generator.rb +554 -0
  24. data/lib/xseed/documentation/generators/instance_sample_generator.rb +723 -0
  25. data/lib/xseed/documentation/generators/properties_table_generator.rb +983 -0
  26. data/lib/xseed/documentation/html_generator.rb +836 -0
  27. data/lib/xseed/documentation/html_generator.rb.bak +723 -0
  28. data/lib/xseed/documentation/presentation/css_generator.rb +510 -0
  29. data/lib/xseed/documentation/presentation/javascript_generator.rb +151 -0
  30. data/lib/xseed/documentation/presentation/navigation_builder.rb +169 -0
  31. data/lib/xseed/documentation/schema_loader.rb +121 -0
  32. data/lib/xseed/documentation/utils/helpers.rb +205 -0
  33. data/lib/xseed/documentation/utils/namespaces.rb +149 -0
  34. data/lib/xseed/documentation/utils/references.rb +135 -0
  35. data/lib/xseed/documentation/utils/strings.rb +75 -0
  36. data/lib/xseed/models/element_declaration.rb +144 -0
  37. data/lib/xseed/parser/xsd_parser.rb +192 -0
  38. data/lib/xseed/version.rb +5 -0
  39. data/lib/xseed.rb +76 -0
  40. data/xseed.gemspec +39 -0
  41. metadata +158 -0
@@ -0,0 +1,723 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "../config"
5
+ require_relative "../constants"
6
+
7
+ module Xseed
8
+ module Documentation
9
+ module Generators
10
+ # Generates XML instance samples for schema components
11
+ # Ported from XS3P instance-samples-*.xsl modules
12
+ class InstanceSampleGenerator
13
+ include Constants
14
+
15
+ attr_reader :component, :parser, :config
16
+
17
+ # Initialize the instance sample generator
18
+ #
19
+ # @param component [Nokogiri::XML::Element] Schema component
20
+ # @param parser [Xseed::Parser::XsdParser] XSD parser instance
21
+ # @param config [Config] Configuration options
22
+ def initialize(component, parser, config = Config.new)
23
+ raise ArgumentError, "Component cannot be nil" if component.nil?
24
+ raise ArgumentError, "Parser cannot be nil" if parser.nil?
25
+
26
+ @component = component
27
+ @parser = parser
28
+ @config = config
29
+ @indent_level = 0
30
+ @type_stack = [] # Track types to prevent infinite recursion
31
+ @group_stack = [] # Track groups to prevent infinite recursion
32
+ @recursion_depth = 0
33
+ @max_recursion_depth = 10
34
+ end
35
+
36
+ # Generate HTML with XML instance sample
37
+ #
38
+ # @return [String] HTML markup
39
+ def generate
40
+ builder = Nokogiri::XML::Builder.new do |xml|
41
+ xml.pre(class: "codehilite") do
42
+ xml << generate_xml_sample_with_highlighting
43
+ end
44
+ end
45
+ builder.doc.root.to_html
46
+ end
47
+
48
+ private
49
+
50
+ # Generate XML sample with syntax highlighting
51
+ #
52
+ # @return [String] HTML with highlighted XML
53
+ def generate_xml_sample_with_highlighting
54
+ xml_text = generate_xml_sample
55
+ highlight_xml(xml_text)
56
+ end
57
+
58
+ # Generate XML sample based on component type
59
+ #
60
+ # @return [String] XML sample text
61
+ def generate_xml_sample
62
+ case component.name
63
+ when "element"
64
+ generate_element_sample
65
+ when "complexType"
66
+ generate_complex_type_sample
67
+ when "simpleType"
68
+ generate_simple_type_sample
69
+ else
70
+ "<!-- Unsupported component type: #{component.name} -->"
71
+ end
72
+ end
73
+
74
+ # Apply syntax highlighting to XML
75
+ #
76
+ # @param xml [String] Plain XML text
77
+ # @return [String] HTML with syntax highlighting
78
+ def highlight_xml(xml)
79
+ result = xml.dup
80
+
81
+ # Highlight XML tags
82
+ result.gsub!(/<(\/?)([\w:]+)([^>]*)>/) do
83
+ tag_close = Regexp.last_match(1)
84
+ tag_name = Regexp.last_match(2)
85
+ rest = Regexp.last_match(3)
86
+
87
+ highlighted = "<span class=\"nt\">&lt;#{tag_close}#{tag_name}"
88
+ highlighted += rest if rest && !rest.empty?
89
+ "#{highlighted}&gt;</span>"
90
+ end
91
+
92
+ # Highlight type information
93
+ result.gsub!(/\b(xsd:\w+)\b/) do
94
+ type_name = Regexp.last_match(1)
95
+ "<span class=\"type\">#{type_name}</span>"
96
+ end
97
+
98
+ # Highlight occurrence info [min..max]
99
+ result.gsub(/(\[[\d∞]+\.\.[\d∞]+\])/) do
100
+ "<span class=\"cs\">#{Regexp.last_match(1)}</span>"
101
+ end
102
+ end
103
+
104
+ # Generate sample for element declaration
105
+ #
106
+ # @return [String] XML sample
107
+ def generate_element_sample
108
+ return "" if prohibited_element?
109
+
110
+ result = []
111
+ indent = " " * @indent_level
112
+
113
+ # Start tag with namespace
114
+ start_tag = "<#{element_tag}"
115
+
116
+ # Add namespace declaration for root element
117
+ if @indent_level.zero? && target_namespace
118
+ start_tag += " xmlns=\"#{target_namespace}\""
119
+ end
120
+
121
+ # Add attributes if complex type or has local complexType
122
+ type_node = nil
123
+ if component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
124
+ type_node = component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
125
+ elsif component["type"]
126
+ type_node = resolve_type
127
+ end
128
+
129
+ if type_node
130
+ attrs = collect_attributes(type_node)
131
+ if attrs && !attrs.empty?
132
+ result << (indent + start_tag) unless start_tag.empty?
133
+ attrs.each do |attr|
134
+ result << "#{indent} #{attr}"
135
+ end
136
+ start_tag = ""
137
+ end
138
+ end
139
+
140
+ # Generate content
141
+ content = generate_element_content
142
+
143
+ if content.empty? && start_tag.end_with?('"')
144
+ # Self-closing tag with attributes
145
+ result << "#{indent}#{start_tag}/>" unless start_tag.empty?
146
+ elsif content.empty?
147
+ # Self-closing tag
148
+ result << "#{indent}#{start_tag}/>"
149
+ else
150
+ # Element with content
151
+ result << "#{indent}#{start_tag}>" if start_tag && !start_tag.empty?
152
+ result << content
153
+ result << "#{indent}</#{element_tag}>"
154
+ end
155
+
156
+ add_occurrence_info(result)
157
+ result.join("\n")
158
+ end
159
+
160
+ # Generate sample for complex type
161
+ #
162
+ # @return [String] XML sample
163
+ def generate_complex_type_sample
164
+ return "" unless component.name == "complexType"
165
+
166
+ type_name = component["name"]
167
+ return "" if circular_type?(type_name)
168
+
169
+ push_type(type_name) if type_name
170
+
171
+ result = []
172
+ indent = " " * @indent_level
173
+
174
+ # Show type structure
175
+ result << "#{indent}<!-- ComplexType: #{type_name} -->" if type_name
176
+
177
+ # Generate content based on content model
178
+ content_result = generate_type_content(component)
179
+ result << content_result unless content_result.empty?
180
+
181
+ pop_type if type_name
182
+
183
+ result.join("\n")
184
+ end
185
+
186
+ # Generate sample for simple type
187
+ #
188
+ # @return [String] XML sample
189
+ def generate_simple_type_sample
190
+ type_name = component["name"]
191
+ restriction = component.at_xpath("xsd:restriction", "xsd" => XSD_NS)
192
+
193
+ return "<!-- SimpleType: #{type_name} -->" unless restriction
194
+
195
+ base_type = restriction["base"] || "string"
196
+ constraints = collect_simple_constraints(restriction)
197
+
198
+ if constraints.empty?
199
+ base_type
200
+ else
201
+ "#{base_type} (#{constraints.join(', ')})"
202
+ end
203
+ end
204
+
205
+ # Generate content for element
206
+ #
207
+ # @return [String] Content text
208
+ def generate_element_content
209
+ return component["fixed"] if component["fixed"]
210
+
211
+ if component["type"]
212
+ type_node = resolve_type
213
+ unless type_node && type_node.name == "complexType"
214
+ return component["type"].split(":").last
215
+ end
216
+
217
+ @indent_level += 1
218
+ content = generate_type_content(type_node)
219
+ @indent_level -= 1
220
+ return content
221
+
222
+ end
223
+
224
+ # Local complex type
225
+ if (local_type = component.at_xpath("xsd:complexType",
226
+ "xsd" => XSD_NS))
227
+ @indent_level += 1
228
+ content = generate_type_content(local_type)
229
+ @indent_level -= 1
230
+ return content
231
+ end
232
+
233
+ # Local simple type
234
+ if (local_type = component.at_xpath("xsd:simpleType",
235
+ "xsd" => XSD_NS))
236
+ return generate_simple_type_content(local_type)
237
+ end
238
+
239
+ "..."
240
+ end
241
+
242
+ # Generate content for complex type
243
+ #
244
+ # @param type [Nokogiri::XML::Element] Complex type node
245
+ # @return [String] Type content
246
+ def generate_type_content(type)
247
+ # Check for complexContent
248
+ if (complex_content = type.at_xpath("xsd:complexContent",
249
+ "xsd" => XSD_NS))
250
+ return generate_complex_content(complex_content)
251
+ end
252
+
253
+ # Check for simpleContent
254
+ if (simple_content = type.at_xpath("xsd:simpleContent",
255
+ "xsd" => XSD_NS))
256
+ return generate_simple_content(simple_content)
257
+ end
258
+
259
+ # Process model groups
260
+ %w[sequence choice all].each do |group_name|
261
+ if (group = type.at_xpath("xsd:#{group_name}", "xsd" => XSD_NS))
262
+ return generate_model_group(group)
263
+ end
264
+ end
265
+
266
+ ""
267
+ end
268
+
269
+ # Generate complex content (extension/restriction)
270
+ #
271
+ # @param complex_content [Nokogiri::XML::Element] complexContent node
272
+ # @return [String] Content
273
+ def generate_complex_content(complex_content)
274
+ parts = []
275
+
276
+ if (extension = complex_content.at_xpath("xsd:extension",
277
+ "xsd" => XSD_NS))
278
+ # Get base type content first
279
+ base_type_name = extension["base"]
280
+ if base_type_name
281
+ base_type = find_type(base_type_name)
282
+ if base_type && !circular_type?(strip_namespace(base_type_name))
283
+ push_type(strip_namespace(base_type_name))
284
+ parts << generate_type_content(base_type)
285
+ pop_type
286
+ end
287
+ end
288
+
289
+ # Add extension content
290
+ %w[sequence choice all].each do |group_name|
291
+ if (group = extension.at_xpath("xsd:#{group_name}",
292
+ "xsd" => XSD_NS))
293
+ parts << generate_model_group(group)
294
+ end
295
+ end
296
+ elsif (restriction = complex_content.at_xpath("xsd:restriction",
297
+ "xsd" => XSD_NS))
298
+ # For restriction, show only the restricted content
299
+ %w[sequence choice all].each do |group_name|
300
+ if (group = restriction.at_xpath("xsd:#{group_name}",
301
+ "xsd" => XSD_NS))
302
+ parts << generate_model_group(group)
303
+ end
304
+ end
305
+ end
306
+
307
+ parts.reject(&:empty?).join("\n")
308
+ end
309
+
310
+ # Generate simple content
311
+ #
312
+ # @param simple_content [Nokogiri::XML::Element] simpleContent node
313
+ # @return [String] Content
314
+ def generate_simple_content(simple_content)
315
+ if (extension = simple_content.at_xpath("xsd:extension",
316
+ "xsd" => XSD_NS))
317
+ base = extension["base"]
318
+ return base ? strip_namespace(base) : "string"
319
+ elsif (restriction = simple_content.at_xpath("xsd:restriction",
320
+ "xsd" => XSD_NS))
321
+ return generate_simple_restriction(restriction)
322
+ end
323
+
324
+ "string"
325
+ end
326
+
327
+ # Generate model group (sequence/choice/all)
328
+ #
329
+ # @param group [Nokogiri::XML::Element] Model group node
330
+ # @return [String] Group content
331
+ def generate_model_group(group)
332
+ return "" if group["maxOccurs"] == "0"
333
+
334
+ # Check recursion depth
335
+ @recursion_depth += 1
336
+ if @recursion_depth > @max_recursion_depth
337
+ @recursion_depth -= 1
338
+ indent = " " * @indent_level
339
+ return "#{indent}<!-- Max recursion depth reached -->"
340
+ end
341
+
342
+ result = []
343
+ indent = " " * @indent_level
344
+ group_name = group.name.capitalize
345
+
346
+ # Show group indicators for choice and occurrence > 1
347
+ show_group = group.name == "choice" ||
348
+ (group["minOccurs"] && group["minOccurs"] != "1") ||
349
+ (group["maxOccurs"] && group["maxOccurs"] != "1")
350
+
351
+ if show_group
352
+ result << "#{indent}<!-- Start #{group_name} #{format_occurs(group)} -->"
353
+ end
354
+
355
+ # Process child elements
356
+ @indent_level += 1 if show_group
357
+
358
+ group.xpath("xsd:element", "xsd" => XSD_NS).each do |elem|
359
+ result << generate_child_element(elem)
360
+ end
361
+
362
+ group.xpath("xsd:group", "xsd" => XSD_NS).each do |grp_ref|
363
+ result << generate_group_reference(grp_ref)
364
+ end
365
+
366
+ @indent_level -= 1 if show_group
367
+
368
+ result << "#{indent}<!-- End #{group_name} -->" if show_group
369
+
370
+ @recursion_depth -= 1
371
+ result.reject(&:empty?).join("\n")
372
+ end
373
+
374
+ # Generate child element within model group
375
+ #
376
+ # @param elem [Nokogiri::XML::Element] Element node
377
+ # @return [String] Element sample
378
+ def generate_child_element(elem)
379
+ return "" if elem["maxOccurs"] == "0"
380
+
381
+ indent = " " * @indent_level
382
+ elem_name = elem["name"] || strip_namespace(elem["ref"] || "element")
383
+
384
+ # Simple content with type
385
+ if elem["type"]
386
+ type_value = strip_namespace(elem["type"])
387
+ occurs = format_occurs(elem)
388
+ return "#{indent}<#{elem_name}>#{type_value}</#{elem_name}> #{occurs}".rstrip
389
+ end
390
+
391
+ # Complex type
392
+ if (local_type = elem.at_xpath("xsd:complexType", "xsd" => XSD_NS))
393
+ result = []
394
+ result << "#{indent}<#{elem_name}>"
395
+ @indent_level += 1
396
+ content = generate_type_content(local_type)
397
+ result << content unless content.empty?
398
+ @indent_level -= 1
399
+ occurs = format_occurs(elem)
400
+ result << "#{indent}</#{elem_name}> #{occurs}".rstrip
401
+ return result.join("\n")
402
+ end
403
+
404
+ # Simple element
405
+ occurs = format_occurs(elem)
406
+ "#{indent}<#{elem_name}>...</#{elem_name}> #{occurs}".rstrip
407
+ end
408
+
409
+ # Generate group reference
410
+ #
411
+ # @param grp_ref [Nokogiri::XML::Element] Group reference node
412
+ # @return [String] Group content
413
+ def generate_group_reference(grp_ref)
414
+ ref_name = strip_namespace(grp_ref["ref"])
415
+
416
+ # Check for circular group reference
417
+ if @group_stack.include?(ref_name)
418
+ indent = " " * @indent_level
419
+ return "#{indent}<!-- Circular reference to group #{ref_name} -->"
420
+ end
421
+
422
+ group_def = find_group(ref_name)
423
+ return "<!-- Group reference: #{ref_name} -->" unless group_def
424
+
425
+ # Track group to prevent circular references
426
+ @group_stack.push(ref_name)
427
+
428
+ # Find the model group within the group definition
429
+ result = ""
430
+ %w[sequence choice all].each do |group_name|
431
+ if (group = group_def.at_xpath("xsd:#{group_name}",
432
+ "xsd" => XSD_NS))
433
+ result = generate_model_group(group)
434
+ break
435
+ end
436
+ end
437
+
438
+ @group_stack.pop
439
+ result
440
+ end
441
+
442
+ # Collect attributes from type
443
+ #
444
+ # @param type [Nokogiri::XML::Element] Type node
445
+ # @return [Array<String>] Attribute samples
446
+ def collect_attributes(type)
447
+ return [] unless type
448
+
449
+ attrs = []
450
+
451
+ # Check simpleContent/extension for attributes
452
+ if (simple_content = type.at_xpath("xsd:simpleContent",
453
+ "xsd" => XSD_NS))
454
+ if (extension = simple_content.at_xpath("xsd:extension",
455
+ "xsd" => XSD_NS))
456
+ attrs.concat(collect_direct_attributes(extension))
457
+ elsif (restriction = simple_content.at_xpath("xsd:restriction",
458
+ "xsd" => XSD_NS))
459
+ attrs.concat(collect_direct_attributes(restriction))
460
+ end
461
+ end
462
+
463
+ # Check complexContent/extension for attributes
464
+ if (complex_content = type.at_xpath("xsd:complexContent",
465
+ "xsd" => XSD_NS))
466
+ if (extension = complex_content.at_xpath("xsd:extension",
467
+ "xsd" => XSD_NS))
468
+ # Get base type attributes first
469
+ base_type_name = extension["base"]
470
+ if base_type_name
471
+ base_type = find_type(base_type_name)
472
+ if base_type && !circular_type?(strip_namespace(base_type_name))
473
+ attrs.concat(collect_attributes(base_type))
474
+ end
475
+ end
476
+ attrs.concat(collect_direct_attributes(extension))
477
+ elsif (restriction = complex_content.at_xpath("xsd:restriction",
478
+ "xsd" => XSD_NS))
479
+ attrs.concat(collect_direct_attributes(restriction))
480
+ end
481
+ end
482
+
483
+ # Direct attributes (for types without simpleContent/complexContent)
484
+ attrs.concat(collect_direct_attributes(type))
485
+
486
+ attrs
487
+ end
488
+
489
+ # Collect direct attributes from a node
490
+ #
491
+ # @param node [Nokogiri::XML::Element] Node to search
492
+ # @return [Array<String>] Attribute samples
493
+ def collect_direct_attributes(node)
494
+ attrs = []
495
+
496
+ # Direct attributes
497
+ node.xpath("xsd:attribute", "xsd" => XSD_NS).each do |attr|
498
+ next if attr["use"] == "prohibited"
499
+
500
+ attr_name = attr["name"] || strip_namespace(attr["ref"] || "attr")
501
+ attr_value = attr["fixed"] || attr["type"] || "string"
502
+ attr_value = strip_namespace(attr_value)
503
+
504
+ use_indicator = attr["use"] == "required" ? "" : "?"
505
+ attrs << "#{attr_name}=\"#{attr_value}\" #{use_indicator}".rstrip
506
+ end
507
+
508
+ # Attribute groups
509
+ node.xpath("xsd:attributeGroup", "xsd" => XSD_NS).each do |attr_grp|
510
+ ref_name = strip_namespace(attr_grp["ref"])
511
+ grp_def = find_attribute_group(ref_name)
512
+ attrs.concat(collect_attributes(grp_def)) if grp_def
513
+ end
514
+
515
+ attrs
516
+ end
517
+
518
+ # Collect simple type constraints
519
+ #
520
+ # @param restriction [Nokogiri::XML::Element] Restriction node
521
+ # @return [Array<String>] Constraint descriptions
522
+ def collect_simple_constraints(restriction)
523
+ constraints = []
524
+
525
+ # Enumeration
526
+ enums = restriction.xpath("xsd:enumeration", "xsd" => XSD_NS)
527
+ if enums.any?
528
+ values = enums.map { |e| e["value"] }.join("|")
529
+ constraints << "enumeration: #{values}"
530
+ end
531
+
532
+ # Pattern
533
+ if (pattern = restriction.at_xpath("xsd:pattern", "xsd" => XSD_NS))
534
+ constraints << "pattern: #{pattern['value']}"
535
+ end
536
+
537
+ # Length constraints
538
+ if (min_len = restriction.at_xpath("xsd:minLength", "xsd" => XSD_NS))
539
+ constraints << "minLength: #{min_len['value']}"
540
+ end
541
+ if (max_len = restriction.at_xpath("xsd:maxLength", "xsd" => XSD_NS))
542
+ constraints << "maxLength: #{max_len['value']}"
543
+ end
544
+
545
+ # Range constraints
546
+ if (min_inc = restriction.at_xpath("xsd:minInclusive",
547
+ "xsd" => XSD_NS))
548
+ constraints << "min: #{min_inc['value']}"
549
+ end
550
+ if (max_inc = restriction.at_xpath("xsd:maxInclusive",
551
+ "xsd" => XSD_NS))
552
+ constraints << "max: #{max_inc['value']}"
553
+ end
554
+
555
+ constraints
556
+ end
557
+
558
+ # Generate simple restriction content
559
+ #
560
+ # @param restriction [Nokogiri::XML::Element] Restriction node
561
+ # @return [String] Content
562
+ def generate_simple_restriction(restriction)
563
+ base = strip_namespace(restriction["base"] || "string")
564
+ constraints = collect_simple_constraints(restriction)
565
+
566
+ if constraints.empty?
567
+ base
568
+ else
569
+ "#{base} (#{constraints.join(', ')})"
570
+ end
571
+ end
572
+
573
+ # Generate simple type content
574
+ #
575
+ # @param simple_type [Nokogiri::XML::Element] Simple type node
576
+ # @return [String] Content
577
+ def generate_simple_type_content(simple_type)
578
+ if (restriction = simple_type.at_xpath("xsd:restriction",
579
+ "xsd" => XSD_NS))
580
+ return generate_simple_restriction(restriction)
581
+ end
582
+
583
+ "string"
584
+ end
585
+
586
+ # Format occurrence information
587
+ #
588
+ # @param node [Nokogiri::XML::Element] Node with occurrence attributes
589
+ # @return [String] Formatted occurrence
590
+ def format_occurs(node)
591
+ min = node["minOccurs"] || "1"
592
+ max = node["maxOccurs"] || "1"
593
+
594
+ return "" if min == "1" && max == "1"
595
+
596
+ max = "∞" if max == "unbounded"
597
+ "[#{min}..#{max}]"
598
+ end
599
+
600
+ # Add occurrence info to result
601
+ #
602
+ # @param result [Array<String>] Result array
603
+ def add_occurrence_info(result)
604
+ return if global_component?
605
+
606
+ occurs = format_occurs(component)
607
+ return if occurs.empty?
608
+
609
+ result[-1] = "#{result[-1]} #{occurs}" if result.any?
610
+ end
611
+
612
+ # Get element tag with namespace prefix if needed
613
+ #
614
+ # @return [String] Element tag
615
+ def element_tag
616
+ component["name"] || strip_namespace(component["ref"] || "element")
617
+ end
618
+
619
+ # Resolve type reference to type definition
620
+ #
621
+ # @return [Nokogiri::XML::Element, nil] Type node
622
+ def resolve_type
623
+ type_ref = component["type"]
624
+ return nil unless type_ref
625
+
626
+ find_type(type_ref)
627
+ end
628
+
629
+ # Find type definition by name
630
+ #
631
+ # @param type_name [String] Type name
632
+ # @return [Nokogiri::XML::Element, nil] Type node
633
+ def find_type(type_name)
634
+ local_name = strip_namespace(type_name)
635
+
636
+ # Check complex types
637
+ parser.complex_types.find { |t| t["name"] == local_name } ||
638
+ # Check simple types
639
+ parser.simple_types.find { |t| t["name"] == local_name }
640
+ end
641
+
642
+ # Find group definition by name
643
+ #
644
+ # @param group_name [String] Group name
645
+ # @return [Nokogiri::XML::Element, nil] Group node
646
+ def find_group(group_name)
647
+ parser.groups.find { |g| g["name"] == group_name }
648
+ end
649
+
650
+ # Find attribute group definition by name
651
+ #
652
+ # @param group_name [String] Attribute group name
653
+ # @return [Nokogiri::XML::Element, nil] Attribute group node
654
+ def find_attribute_group(group_name)
655
+ parser.attribute_groups.find { |ag| ag["name"] == group_name }
656
+ end
657
+
658
+ # Check if element has complex type
659
+ #
660
+ # @return [Boolean]
661
+ def has_complex_type?
662
+ return true if component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
663
+ return false unless component["type"]
664
+
665
+ type_node = resolve_type
666
+ type_node && type_node.name == "complexType"
667
+ end
668
+
669
+ # Check if element is prohibited
670
+ #
671
+ # @return [Boolean]
672
+ def prohibited_element?
673
+ component["maxOccurs"] == "0"
674
+ end
675
+
676
+ # Check if component is global
677
+ #
678
+ # @return [Boolean]
679
+ def global_component?
680
+ component.parent&.name == "schema"
681
+ end
682
+
683
+ # Get target namespace
684
+ #
685
+ # @return [String, nil] Target namespace
686
+ def target_namespace
687
+ schema = component.document.root
688
+ schema["targetNamespace"] if schema
689
+ end
690
+
691
+ # Strip namespace prefix from name
692
+ #
693
+ # @param name [String] Qualified name
694
+ # @return [String] Local name
695
+ def strip_namespace(name)
696
+ name.to_s.split(":").last
697
+ end
698
+
699
+ # Check if type is circular
700
+ #
701
+ # @param type_name [String] Type name
702
+ # @return [Boolean]
703
+ def circular_type?(type_name)
704
+ return false unless type_name
705
+
706
+ @type_stack.include?(type_name)
707
+ end
708
+
709
+ # Push type onto stack
710
+ #
711
+ # @param type_name [String] Type name
712
+ def push_type(type_name)
713
+ @type_stack.push(type_name) if type_name
714
+ end
715
+
716
+ # Pop type from stack
717
+ def pop_type
718
+ @type_stack.pop
719
+ end
720
+ end
721
+ end
722
+ end
723
+ end