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,983 @@
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 HTML properties definition lists for schema components
11
+ # Ported from XS3P xs3p.xsl properties templates (lines 2322-3418)
12
+ #
13
+ # XS3P uses definition lists (<dl class="dl-horizontal">) not tables.
14
+ # Each component type generates 1-3 DLs:
15
+ # - Elements: Properties DL + Documentation DL
16
+ # - Complex Types: Used By DL + Properties DL + Documentation DL
17
+ # - Simple Types: Properties DL + Documentation DL
18
+ # - Attributes: Properties DL + Documentation DL
19
+ # - Attribute Groups/Groups: Used By DL + Documentation DL
20
+ # - Schema: Properties DL + Namespaces DL
21
+ class PropertiesTableGenerator
22
+ include Constants
23
+
24
+ attr_reader :component, :config, :schema
25
+
26
+ # Initialize the properties generator
27
+ #
28
+ # @param component [Nokogiri::XML::Element] Schema component
29
+ # @param config [Config] Configuration options
30
+ def initialize(component, config = Config.new)
31
+ raise ArgumentError, "Component cannot be nil" if component.nil?
32
+
33
+ @component = component
34
+ @config = config
35
+ @schema = component.document.root
36
+ end
37
+
38
+ # Generate HTML definition lists with component properties
39
+ # Returns array of DL HTML strings (1-3 DLs depending on component type)
40
+ #
41
+ # @return [Array<String>] Array of HTML DL markup strings
42
+ def generate
43
+ case component.name
44
+ when "schema"
45
+ generate_schema_properties
46
+ when "element"
47
+ generate_element_properties
48
+ when "complexType"
49
+ generate_complex_type_properties
50
+ when "simpleType"
51
+ generate_simple_type_properties
52
+ when "attribute"
53
+ generate_attribute_properties
54
+ when "attributeGroup", "group"
55
+ generate_group_properties
56
+ when "notation"
57
+ generate_notation_properties
58
+ else
59
+ []
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Generate properties for schema element (xs3p.xsl lines 3088-3290)
66
+ # Returns: [Properties DL, Namespaces DL]
67
+ def generate_schema_properties
68
+ dls = []
69
+
70
+ # First DL: Schema properties
71
+ dls << build_dl do |xml|
72
+ # Target Namespace
73
+ xml.dt(class: "header") do
74
+ glossary_term_ref(xml, "TargetNS", "Target Namespace")
75
+ end
76
+ xml.dd(class: "") do
77
+ if schema["targetNamespace"]
78
+ xml.span(class: "targetNS") do
79
+ xml.text schema["targetNamespace"]
80
+ end
81
+ else
82
+ xml.text "None"
83
+ end
84
+ end
85
+
86
+ # Version
87
+ if schema["version"]
88
+ xml.dt(class: "header") { xml.text "Version" }
89
+ xml.dd(class: "") { xml.text schema["version"] }
90
+ end
91
+
92
+ # Language
93
+ if schema["xml:lang"]
94
+ xml.dt(class: "header") { xml.text "Language" }
95
+ xml.dd(class: "") { xml.text schema["xml:lang"] }
96
+ end
97
+
98
+ # Element and Attribute Namespaces
99
+ xml.dt(class: "header") do
100
+ xml.text "Element and Attribute Namespaces"
101
+ end
102
+ xml.dd(class: "") do
103
+ xml.ul do
104
+ xml.li do
105
+ xml.text "Global element and attribute declarations belong to this schema's target namespace."
106
+ end
107
+ xml.li do
108
+ if schema["elementFormDefault"] == "qualified"
109
+ xml.text "By default, local element declarations belong to this schema's target namespace."
110
+ else
111
+ xml.text "By default, local element declarations have no namespace."
112
+ end
113
+ end
114
+ xml.li do
115
+ if schema["attributeFormDefault"] == "qualified"
116
+ xml.text "By default, local attribute declarations belong to this schema's target namespace."
117
+ else
118
+ xml.text "By default, local attribute declarations have no namespace."
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Schema Composition (imports, includes, redefines)
125
+ if has_schema_composition?
126
+ xml.dt(class: "header") { xml.text "Schema Composition" }
127
+ xml.dd(class: "") do
128
+ xml.ul do
129
+ generate_composition_info(xml)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ # Second DL: Declared Namespaces
136
+ dls << build_declared_namespaces_dl
137
+
138
+ dls
139
+ end
140
+
141
+ # Generate properties for element (xs3p.xsl lines 2732-2947)
142
+ # Returns: [Properties DL, Documentation DL]
143
+ def generate_element_properties
144
+ dls = []
145
+
146
+ # First DL: Element properties
147
+ dls << build_dl do |xml|
148
+ # Type
149
+ xml.dt(class: "header") { xml.text "Type" }
150
+ xml.dd(class: "") do
151
+ type_value = get_element_type
152
+ if type_value.start_with?("Locally-defined")
153
+ xml.text type_value
154
+ else
155
+ xml.span(class: "type") do
156
+ type_ref_link(xml, type_value)
157
+ end
158
+ end
159
+ end
160
+
161
+ # Used By
162
+ used_by = find_used_by_for_element
163
+ if used_by.any?
164
+ xml.dt(class: "header") { xml.text "Used By" }
165
+ xml.dd(class: "") do
166
+ used_by.each_with_index do |type_name, idx|
167
+ xml.text ", " if idx.positive?
168
+ xml.span(class: "type") { type_ref_link(xml, type_name) }
169
+ end
170
+ end
171
+ end
172
+
173
+ # Nillable
174
+ if component["nillable"]
175
+ xml.dt(class: "header") do
176
+ glossary_term_ref(xml, "Nillable", "Nillable")
177
+ end
178
+ xml.dd(class: "") do
179
+ xml.text print_boolean(component["nillable"])
180
+ end
181
+ end
182
+
183
+ # Abstract
184
+ if component["abstract"]
185
+ xml.dt(class: "header") do
186
+ glossary_term_ref(xml, "Abstract", "Abstract")
187
+ end
188
+ xml.dd(class: "") do
189
+ xml.text print_boolean(component["abstract"])
190
+ end
191
+ end
192
+
193
+ # Default Value
194
+ if component["default"]
195
+ xml.dt(class: "header") { xml.text "Default Value" }
196
+ xml.dd(class: "") { xml.text component["default"] }
197
+ end
198
+
199
+ # Fixed Value
200
+ if component["fixed"]
201
+ xml.dt(class: "header") { xml.text "Fixed Value" }
202
+ xml.dd(class: "") { xml.text component["fixed"] }
203
+ end
204
+
205
+ # Final (Substitution Group Exclusions)
206
+ final_value = get_final_value
207
+ if final_value && !final_value.empty?
208
+ xml.dt(class: "header") do
209
+ glossary_term_ref(xml, "ElemFinal",
210
+ "Substitution Group Exclusions")
211
+ end
212
+ xml.dd(class: "") { xml.text final_value }
213
+ end
214
+
215
+ # Block (Disallowed Substitutions)
216
+ block_value = get_block_value
217
+ if block_value && !block_value.empty?
218
+ xml.dt(class: "header") do
219
+ glossary_term_ref(xml, "ElemBlock", "Disallowed Substitutions")
220
+ end
221
+ xml.dd(class: "") { xml.text block_value }
222
+ end
223
+ end
224
+
225
+ # Second DL: Documentation
226
+ dls << generate_documentation_dl
227
+
228
+ dls.compact
229
+ end
230
+
231
+ # Generate properties for complex type (xs3p.xsl lines 2547-2726)
232
+ # Returns: [Super-types DL, Used By DL, Properties DL, Documentation DL]
233
+ def generate_complex_type_properties
234
+ dls = []
235
+
236
+ # First DL: Super-types (if has extension/restriction)
237
+ base_type = get_complex_type_base
238
+ if base_type
239
+ dls << build_dl do |xml|
240
+ xml.dt(class: "header") { xml.text "Super-types:" }
241
+ xml.dd(class: "") do
242
+ xml.span(class: "type") { type_ref_link(xml, base_type) }
243
+ end
244
+ end
245
+ end
246
+
247
+ # Second DL: Used By (if applicable)
248
+ used_by = find_used_by_for_type
249
+ if used_by.any?
250
+ dls << build_dl do |xml|
251
+ xml.dt(class: "header") { xml.text "Used By" }
252
+ xml.dd(class: "") do
253
+ used_by.each_with_index do |elem_name, idx|
254
+ xml.text ", " if idx.positive?
255
+ xml.span(class: "type") { element_ref_link(xml, elem_name) }
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ # Third DL: Complex type properties (ONLY if has abstract/final/block)
262
+ if has_complex_type_properties?
263
+ dls << build_dl do |xml|
264
+ # Abstract
265
+ if component["abstract"]
266
+ xml.dt(class: "header") do
267
+ glossary_term_ref(xml, "Abstract", "Abstract")
268
+ end
269
+ xml.dd(class: "") do
270
+ xml.text print_boolean(component["abstract"])
271
+ end
272
+ end
273
+
274
+ # Final (Prohibited Derivations)
275
+ final_value = get_derivation_set(component["final"] || schema["finalDefault"])
276
+ unless final_value.empty?
277
+ xml.dt(class: "header") do
278
+ glossary_term_ref(xml, "TypeFinal", "Prohibited Derivations")
279
+ end
280
+ xml.dd(class: "") { xml.text final_value }
281
+ end
282
+
283
+ # Block (Prohibited Substitutions)
284
+ block_value = get_derivation_set(component["block"] || schema["blockDefault"])
285
+ unless block_value.empty?
286
+ xml.dt(class: "header") do
287
+ glossary_term_ref(xml, "TypeBlock",
288
+ "Prohibited Substitutions")
289
+ end
290
+ xml.dd(class: "") { xml.text block_value }
291
+ end
292
+ end
293
+ end
294
+
295
+ # Fourth DL: Documentation
296
+ dls << generate_documentation_dl
297
+
298
+ dls.compact
299
+ end
300
+
301
+ # Get base type for complex type (from extension or restriction)
302
+ # Returns base type name or nil
303
+ def get_complex_type_base
304
+ # Check for complexContent/extension
305
+ extension = component.at_xpath("xsd:complexContent/xsd:extension",
306
+ "xsd" => XSD_NS)
307
+ return extension["base"] if extension
308
+
309
+ # Check for complexContent/restriction
310
+ restriction = component.at_xpath("xsd:complexContent/xsd:restriction",
311
+ "xsd" => XSD_NS)
312
+ return restriction["base"] if restriction
313
+
314
+ # Check for simpleContent/extension
315
+ extension = component.at_xpath("xsd:simpleContent/xsd:extension",
316
+ "xsd" => XSD_NS)
317
+ return extension["base"] if extension
318
+
319
+ # Check for simpleContent/restriction
320
+ restriction = component.at_xpath("xsd:simpleContent/xsd:restriction",
321
+ "xsd" => XSD_NS)
322
+ restriction["base"] if restriction
323
+ end
324
+
325
+ # Check if complex type has properties worth displaying (follows XS3P logic)
326
+ # Only generate Properties DL if has abstract/final/block attributes
327
+ def has_complex_type_properties?
328
+ return true if component["abstract"]
329
+
330
+ final_value = get_derivation_set(component["final"] || schema["finalDefault"])
331
+ return true unless final_value.empty?
332
+
333
+ block_value = get_derivation_set(component["block"] || schema["blockDefault"])
334
+ !block_value.empty?
335
+ end
336
+
337
+ # Generate properties for simple type (xs3p.xsl lines 3297-3412)
338
+ # Returns: [Properties DL, Documentation DL]
339
+ def generate_simple_type_properties
340
+ dls = []
341
+
342
+ # First DL: Simple type properties
343
+ dls << build_dl do |xml|
344
+ # Content (with facets)
345
+ xml.dt(class: "header") { xml.text "Content" }
346
+ xml.dd(class: "") do
347
+ print_simple_constraints(xml)
348
+ end
349
+
350
+ # Final (Prohibited Derivations)
351
+ final_value = get_simple_derivation_set(component["final"] || schema["finalDefault"])
352
+ unless final_value.empty?
353
+ xml.dt(class: "header") do
354
+ glossary_term_ref(xml, "TypeFinal", "Prohibited Derivations")
355
+ end
356
+ xml.dd(class: "") { xml.text final_value }
357
+ end
358
+ end
359
+
360
+ # Second DL: Documentation
361
+ dls << generate_documentation_dl
362
+
363
+ dls.compact
364
+ end
365
+
366
+ # Generate properties for attribute (xs3p.xsl lines 2322-2435)
367
+ # Returns: [Properties DL, Documentation DL]
368
+ def generate_attribute_properties
369
+ dls = []
370
+
371
+ # First DL: Attribute properties
372
+ dls << build_dl do |xml|
373
+ # Type
374
+ xml.dt(class: "header") { xml.text "Type" }
375
+ xml.dd(class: "") do
376
+ type_value = get_attribute_type
377
+ if type_value.start_with?("Locally-defined")
378
+ xml.text type_value
379
+ else
380
+ xml.span(class: "type") { type_ref_link(xml, type_value) }
381
+ end
382
+ end
383
+
384
+ # Default Value
385
+ if component["default"]
386
+ xml.dt(class: "header") { xml.text "Default Value" }
387
+ xml.dd(class: "") { xml.text component["default"] }
388
+ end
389
+
390
+ # Fixed Value
391
+ if component["fixed"]
392
+ xml.dt(class: "header") { xml.text "Fixed Value" }
393
+ xml.dd(class: "") { xml.text component["fixed"] }
394
+ end
395
+ end
396
+
397
+ # Second DL: Documentation
398
+ dls << generate_documentation_dl
399
+
400
+ dls.compact
401
+ end
402
+
403
+ # Generate properties for attribute group or model group (xs3p.xsl lines 2441-2541)
404
+ # Returns: [Used By DL (optional), Documentation DL]
405
+ def generate_group_properties
406
+ dls = []
407
+
408
+ # First DL: Used By (if applicable)
409
+ if component.name == "attributeGroup"
410
+ used_by = find_used_by_for_attribute_group
411
+ if used_by.any?
412
+ dls << build_dl do |xml|
413
+ xml.dt(class: "header") { xml.text "Used By" }
414
+ xml.dd(class: "") do
415
+ used_by.each_with_index do |type_name, idx|
416
+ xml.text ", " if idx.positive?
417
+ xml.span(class: "type") { type_ref_link(xml, type_name) }
418
+ end
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ # Second DL: Documentation
425
+ dls << generate_documentation_dl
426
+
427
+ dls.compact
428
+ end
429
+
430
+ # Generate properties for notation (xs3p.xsl lines 2989-3082)
431
+ # Returns: [Properties DL, Documentation DL]
432
+ def generate_notation_properties
433
+ dls = []
434
+
435
+ # First DL: Notation properties
436
+ dls << build_dl do |xml|
437
+ # Public Identifier
438
+ xml.dt(class: "header") { xml.text "Public Identifier" }
439
+ xml.dd(class: "") { xml.text component["public"] }
440
+
441
+ # System Identifier
442
+ if component["system"]
443
+ xml.dt(class: "header") { xml.text "System Identifier" }
444
+ xml.dd(class: "") { xml.text component["system"] }
445
+ end
446
+ end
447
+
448
+ # Second DL: Documentation
449
+ dls << generate_documentation_dl
450
+
451
+ dls.compact
452
+ end
453
+
454
+ # Generate documentation DL for any component
455
+ # Returns nil if no documentation exists
456
+ def generate_documentation_dl
457
+ doc_content = extract_documentation
458
+ return nil unless doc_content
459
+
460
+ build_dl do |xml|
461
+ xml.dt { xml.text "Documentation" }
462
+ xml.dd do
463
+ xml.div(class: "annotation documentation",
464
+ id: "wdoc-#{component.object_id}") do
465
+ xml.div(class: "hidden",
466
+ id: "#{component.object_id}-doc-raw") do
467
+ xml.text doc_content
468
+ end
469
+ xml.div(class: "xs3p-doc", id: "#{component.object_id}-doc") do
470
+ xml.text " "
471
+ end
472
+ end
473
+ end
474
+ end
475
+ end
476
+
477
+ # Generate declared namespaces DL for schema
478
+ def build_declared_namespaces_dl
479
+ build_dl do |xml|
480
+ # Header row
481
+ xml.dt(class: "header") { xml.text "Prefix" }
482
+ xml.dd(class: "header") { xml.text "Namespace" }
483
+
484
+ # Default namespace
485
+ default_ns = schema.namespaces["xmlns"]
486
+ if default_ns
487
+ xml.dt(class: "") do
488
+ xml.a(id: "ns_") { xml.text "Default namespace" }
489
+ end
490
+ xml.dd(class: "") do
491
+ if default_ns == schema["targetNamespace"]
492
+ xml.span(class: "targetNS") { xml.text default_ns }
493
+ else
494
+ xml.text default_ns
495
+ end
496
+ end
497
+ end
498
+
499
+ # Namespaces with prefixes
500
+ schema.namespaces.each do |prefix_key, namespace_uri|
501
+ next if prefix_key == "xmlns" # Skip default namespace
502
+
503
+ prefix = prefix_key.sub("xmlns:", "")
504
+ xml.dt(class: "") do
505
+ xml.a(id: "ns_#{prefix}") { xml.text prefix }
506
+ end
507
+ xml.dd(class: "") do
508
+ if namespace_uri == schema["targetNamespace"]
509
+ xml.span(class: "targetNS") { xml.text namespace_uri }
510
+ else
511
+ xml.text namespace_uri
512
+ end
513
+ end
514
+ end
515
+ end
516
+ end
517
+
518
+ # Build a definition list with given content
519
+ def build_dl
520
+ builder = Nokogiri::XML::Builder.new do |xml|
521
+ xml.dl(class: "dl-horizontal") do
522
+ yield(xml)
523
+ end
524
+ end
525
+ builder.doc.root.to_html
526
+ end
527
+
528
+ # Print simple constraints for simple types (xs3p.xsl lines 3573-3813)
529
+ def print_simple_constraints(xml)
530
+ restriction = component.at_xpath("xsd:restriction", "xsd" => XSD_NS)
531
+ list_elem = component.at_xpath("xsd:list", "xsd" => XSD_NS)
532
+ union_elem = component.at_xpath("xsd:union", "xsd" => XSD_NS)
533
+
534
+ if restriction
535
+ print_simple_restriction(xml, restriction)
536
+ elsif list_elem
537
+ xml.ul do
538
+ xml.li do
539
+ xml.text "List of: "
540
+ if list_elem["itemType"]
541
+ type_ref_link(xml, list_elem["itemType"])
542
+ else
543
+ # Locally-defined item type
544
+ xml.text "Locally defined type"
545
+ end
546
+ end
547
+ end
548
+ elsif union_elem
549
+ xml.ul do
550
+ xml.li do
551
+ xml.text "Union of following types: "
552
+ xml.ul do
553
+ union_elem["memberTypes"]&.split&.each do |member_type|
554
+ xml.li { type_ref_link(xml, member_type) }
555
+ end
556
+ # Locally-defined member types
557
+ union_elem.xpath("xsd:simpleType", "xsd" => XSD_NS).each do
558
+ xml.li { xml.text "Locally defined type" }
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end
565
+
566
+ # Print simple restriction (xs3p.xsl lines 3651-3734)
567
+ def print_simple_restriction(xml, restriction)
568
+ base_type = restriction["base"]
569
+
570
+ # Base type
571
+ if base_type
572
+ base_name = base_type.include?(":") ? base_type.split(":").last : base_type
573
+ base_ns = get_namespace_for_prefix(base_type.split(":").first) if base_type.include?(":")
574
+
575
+ if base_ns == XSD_NS || !base_type.include?(":")
576
+ xml.ul do
577
+ xml.li do
578
+ xml.text "Base XSD Type: "
579
+ xml.text base_name
580
+ end
581
+ end
582
+ else
583
+ # Look up the base type and recurse
584
+ base_type_elem = schema.at_xpath(
585
+ "//xsd:simpleType[@name='#{base_name}']", "xsd" => XSD_NS
586
+ )
587
+ if base_type_elem
588
+ base_gen = self.class.new(base_type_elem, config)
589
+ base_gen.print_simple_constraints(xml)
590
+ end
591
+ end
592
+ end
593
+
594
+ # Facets
595
+ print_facets(xml, restriction)
596
+ end
597
+
598
+ # Print facets from restriction (xs3p.xsl lines 3737-3811)
599
+ def print_facets(xml, restriction)
600
+ facets_list = []
601
+
602
+ # Enumeration
603
+ enums = restriction.xpath("xsd:enumeration", "xsd" => XSD_NS)
604
+ if enums.any?
605
+ facets_list << ->(xml) do
606
+ xml.em { xml.text "value" }
607
+ xml.text " comes from list: {"
608
+ enums.each_with_index do |enum, idx|
609
+ xml.text "|" if idx.positive?
610
+ xml.text "'#{enum['value']}'"
611
+ end
612
+ xml.text "}"
613
+ end
614
+ end
615
+
616
+ # Pattern
617
+ pattern = restriction.at_xpath("xsd:pattern", "xsd" => XSD_NS)
618
+ if pattern
619
+ facets_list << ->(xml) do
620
+ xml.em { xml.text "pattern" }
621
+ xml.text " = #{pattern['value']}"
622
+ end
623
+ end
624
+
625
+ # Range facets
626
+ range_facet = get_range_facet(restriction)
627
+ if range_facet
628
+ facets_list << ->(xml) { xml << range_facet }
629
+ end
630
+
631
+ # Total digits
632
+ total_digits = restriction.at_xpath("xsd:totalDigits",
633
+ "xsd" => XSD_NS)
634
+ if total_digits
635
+ facets_list << ->(xml) do
636
+ xml.em { xml.text "total no. of digits" }
637
+ xml.text " = #{total_digits['value']}"
638
+ end
639
+ end
640
+
641
+ # Fraction digits
642
+ fraction_digits = restriction.at_xpath("xsd:fractionDigits",
643
+ "xsd" => XSD_NS)
644
+ if fraction_digits
645
+ facets_list << ->(xml) do
646
+ xml.em { xml.text "no. of fraction digits" }
647
+ xml.text " = #{fraction_digits['value']}"
648
+ end
649
+ end
650
+
651
+ # Length facets
652
+ length_facet = get_length_facet(restriction)
653
+ if length_facet
654
+ facets_list << ->(xml) { xml << length_facet }
655
+ end
656
+
657
+ # Whitespace
658
+ whitespace = restriction.at_xpath("xsd:whiteSpace", "xsd" => XSD_NS)
659
+ if whitespace
660
+ facets_list << ->(xml) do
661
+ xml.em { xml.text "Whitespace policy: " }
662
+ policy_code = case whitespace["value"]
663
+ when "preserve" then "PreserveWS"
664
+ when "replace" then "ReplaceWS"
665
+ when "collapse" then "CollapseWS"
666
+ end
667
+ if policy_code
668
+ glossary_term_ref(xml, policy_code,
669
+ whitespace["value"])
670
+ end
671
+ end
672
+ end
673
+
674
+ # Output facets as list if any exist
675
+ return if facets_list.empty?
676
+
677
+ xml.ul do
678
+ facets_list.each do |facet_lambda|
679
+ xml.li { facet_lambda.call(xml) }
680
+ end
681
+ end
682
+ end
683
+
684
+ # Get range facet string
685
+ def get_range_facet(restriction)
686
+ min_inc = restriction.at_xpath("xsd:minInclusive", "xsd" => XSD_NS)
687
+ min_exc = restriction.at_xpath("xsd:minExclusive", "xsd" => XSD_NS)
688
+ max_inc = restriction.at_xpath("xsd:maxInclusive", "xsd" => XSD_NS)
689
+ max_exc = restriction.at_xpath("xsd:maxExclusive", "xsd" => XSD_NS)
690
+
691
+ return nil unless min_inc || min_exc || max_inc || max_exc
692
+
693
+ parts = []
694
+ if min_inc
695
+ parts << "#{min_inc['value']} &lt;= <em>value</em>"
696
+ elsif min_exc
697
+ parts << "#{min_exc['value']} &lt; <em>value</em>"
698
+ end
699
+
700
+ if max_inc
701
+ parts << "<em>value</em> &lt;= #{max_inc['value']}"
702
+ elsif max_exc
703
+ parts << "<em>value</em> &lt; #{max_exc['value']}"
704
+ end
705
+
706
+ parts.join(" and ")
707
+ end
708
+
709
+ # Get length facet string
710
+ def get_length_facet(restriction)
711
+ length = restriction.at_xpath("xsd:length", "xsd" => XSD_NS)
712
+ min_length = restriction.at_xpath("xsd:minLength", "xsd" => XSD_NS)
713
+ max_length = restriction.at_xpath("xsd:maxLength", "xsd" => XSD_NS)
714
+
715
+ return nil unless length || min_length || max_length
716
+
717
+ if length
718
+ "<em>length</em> = #{length['value']}"
719
+ elsif min_length && max_length
720
+ "#{min_length['value']} &lt;= <em>length</em> &lt;= #{max_length['value']}"
721
+ elsif min_length
722
+ "<em>length</em> &gt;= #{min_length['value']}"
723
+ elsif max_length
724
+ "<em>length</em> &lt;= #{max_length['value']}"
725
+ end
726
+ end
727
+
728
+ # Find elements that use this element (via ref)
729
+ def find_used_by_for_element
730
+ elem_name = component["name"]
731
+ return [] unless elem_name
732
+
733
+ used_by = []
734
+ schema.xpath("//xsd:element[@ref='#{elem_name}']",
735
+ "xsd" => XSD_NS).each do |ref_elem|
736
+ # Find containing complex type
737
+ parent_type = ref_elem.at_xpath("ancestor::xsd:complexType[@name]",
738
+ "xsd" => XSD_NS)
739
+ used_by << parent_type["name"] if parent_type
740
+ end
741
+ used_by.uniq
742
+ end
743
+
744
+ # Find elements that use this type
745
+ def find_used_by_for_type
746
+ type_name = component["name"]
747
+ return [] unless type_name
748
+
749
+ used_by = schema.xpath(
750
+ "//xsd:element[@type='#{type_name}'] | //xsd:element[@type='#{get_prefixed_name(type_name)}']", "xsd" => XSD_NS
751
+ ).map do |elem|
752
+ elem["name"]
753
+ end
754
+ used_by.uniq.compact
755
+ end
756
+
757
+ # Find types that use this attribute group
758
+ def find_used_by_for_attribute_group
759
+ group_name = component["name"]
760
+ return [] unless group_name
761
+
762
+ used_by = []
763
+ schema.xpath("//xsd:attributeGroup[@ref='#{group_name}']",
764
+ "xsd" => XSD_NS).each do |ref|
765
+ parent_type = ref.at_xpath("ancestor::xsd:complexType[@name]",
766
+ "xsd" => XSD_NS)
767
+ used_by << parent_type["name"] if parent_type
768
+ end
769
+ used_by.uniq
770
+ end
771
+
772
+ # Get element type
773
+ def get_element_type
774
+ if component.at_xpath("xsd:simpleType", "xsd" => XSD_NS)
775
+ "Locally-defined simple type"
776
+ elsif component.at_xpath("xsd:complexType", "xsd" => XSD_NS)
777
+ "Locally-defined complex type"
778
+ elsif component["type"]
779
+ component["type"]
780
+ else
781
+ "anyType"
782
+ end
783
+ end
784
+
785
+ # Get attribute type
786
+ def get_attribute_type
787
+ if component.at_xpath("xsd:simpleType", "xsd" => XSD_NS)
788
+ "Locally-defined simple type"
789
+ elsif component["type"]
790
+ component["type"]
791
+ else
792
+ "anySimpleType"
793
+ end
794
+ end
795
+
796
+ # Get final value for element
797
+ def get_final_value
798
+ final_attr = component["final"] || schema["finalDefault"]
799
+ translate_derivation_set(final_attr)
800
+ end
801
+
802
+ # Get block value for element
803
+ def get_block_value
804
+ block_attr = component["block"] || schema["blockDefault"]
805
+ translate_block_set(block_attr)
806
+ end
807
+
808
+ # Translate derivation set (#all -> full list)
809
+ def get_derivation_set(value)
810
+ return "" unless value
811
+
812
+ if value == "#all"
813
+ "restriction, extension"
814
+ else
815
+ value
816
+ end
817
+ end
818
+
819
+ # Translate simple derivation set (#all -> full list)
820
+ def get_simple_derivation_set(value)
821
+ return "" unless value
822
+
823
+ if value == "#all"
824
+ "restriction, list, union"
825
+ else
826
+ value
827
+ end
828
+ end
829
+
830
+ # Translate block set (#all -> full list for elements)
831
+ def translate_block_set(value)
832
+ return "" unless value
833
+
834
+ if value == "#all"
835
+ "restriction, extension, substitution"
836
+ else
837
+ value
838
+ end
839
+ end
840
+
841
+ # Translate derivation set for elements/types
842
+ def translate_derivation_set(value)
843
+ return "" unless value
844
+
845
+ if value == "#all"
846
+ "restriction, extension"
847
+ else
848
+ value
849
+ end
850
+ end
851
+
852
+ # Print boolean value as yes/no
853
+ def print_boolean(bool_value)
854
+ return "no" unless bool_value
855
+
856
+ normalized = bool_value.to_s.downcase
857
+ ["true", "1"].include?(normalized) ? "yes" : "no"
858
+ end
859
+
860
+ # Check if schema has imports, includes, or redefines
861
+ def has_schema_composition?
862
+ schema.at_xpath("xsd:import | xsd:include | xsd:redefine",
863
+ "xsd" => XSD_NS)
864
+ end
865
+
866
+ # Generate schema composition info
867
+ def generate_composition_info(xml)
868
+ # Imports
869
+ imports = schema.xpath("xsd:import", "xsd" => XSD_NS)
870
+ if imports.any?
871
+ xml.li do
872
+ xml.text "This schema imports schema(s) from the following namespace(s):"
873
+ xml.ul do
874
+ imports.each do |import_elem|
875
+ xml.li do
876
+ xml.em { xml.text import_elem["namespace"] }
877
+ if import_elem["schemaLocation"]
878
+ xml.text " (at #{import_elem['schemaLocation']})"
879
+ end
880
+ end
881
+ end
882
+ end
883
+ end
884
+ end
885
+
886
+ # Includes
887
+ includes = schema.xpath("xsd:include", "xsd" => XSD_NS)
888
+ if includes.any?
889
+ xml.li do
890
+ xml.text "This schema includes components from the following schema document(s):"
891
+ xml.ul do
892
+ includes.each do |include_elem|
893
+ xml.li { xml.text include_elem["schemaLocation"] }
894
+ end
895
+ end
896
+ end
897
+ end
898
+
899
+ # Redefines
900
+ redefines = schema.xpath("xsd:redefine", "xsd" => XSD_NS)
901
+ return unless redefines.any?
902
+
903
+ xml.li do
904
+ xml.text "This schema includes components from the following schema document(s), where some of the components have been redefined:"
905
+ xml.ul do
906
+ redefines.each do |redefine_elem|
907
+ xml.li { xml.text redefine_elem["schemaLocation"] }
908
+ end
909
+ end
910
+ xml.text "See "
911
+ xml.a(href: "#Redefinitions") do
912
+ xml.text "Redefined Schema Components"
913
+ end
914
+ xml.text " section."
915
+ end
916
+ end
917
+
918
+ # Extract documentation text
919
+ def extract_documentation
920
+ doc_node = component.at_xpath("xsd:annotation/xsd:documentation",
921
+ "xsd" => XSD_NS)
922
+ doc_node&.text&.strip
923
+ end
924
+
925
+ # Generate glossary term reference link
926
+ def glossary_term_ref(xml, code, term)
927
+ if config.print_glossary
928
+ xml.a(title: "Look up '#{term}' in glossary",
929
+ href: "#term_#{code}") do
930
+ xml.text term
931
+ end
932
+ else
933
+ xml.text term
934
+ end
935
+ end
936
+
937
+ # Generate type reference link
938
+ def type_ref_link(xml, type_ref)
939
+ type_name = type_ref.include?(":") ? type_ref.split(":").last : type_ref
940
+ xml.a(title: "Jump to \"#{type_name}\" type definition.",
941
+ href: "#type_#{type_name}") do
942
+ xml.text type_name
943
+ end
944
+ end
945
+
946
+ # Generate element reference link
947
+ def element_ref_link(xml, elem_ref)
948
+ elem_name = elem_ref.include?(":") ? elem_ref.split(":").last : elem_ref
949
+ xml.a(title: "Jump to \"#{elem_name}\" element declaration.",
950
+ href: "#element_#{elem_name}") do
951
+ xml.text elem_name
952
+ end
953
+ end
954
+
955
+ # Get prefixed name for type reference
956
+ def get_prefixed_name(name)
957
+ prefix = get_target_namespace_prefix
958
+ prefix ? "#{prefix}:#{name}" : name
959
+ end
960
+
961
+ # Get prefix for target namespace
962
+ def get_target_namespace_prefix
963
+ target_ns = schema["targetNamespace"]
964
+ return nil unless target_ns
965
+
966
+ schema.namespaces.each do |prefix_key, ns_uri|
967
+ next if prefix_key == "xmlns"
968
+
969
+ return prefix_key.sub("xmlns:", "") if ns_uri == target_ns
970
+ end
971
+ nil
972
+ end
973
+
974
+ # Get namespace URI for a given prefix
975
+ def get_namespace_for_prefix(prefix)
976
+ return nil unless prefix
977
+
978
+ schema.namespaces["xmlns:#{prefix}"]
979
+ end
980
+ end
981
+ end
982
+ end
983
+ end