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,836 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "config"
5
+ require_relative "constants"
6
+ require_relative "utils/references"
7
+ require_relative "utils/namespaces"
8
+ require_relative "utils/helpers"
9
+ require_relative "generators/properties_table_generator"
10
+ require_relative "generators/hierarchy_table_generator"
11
+ require_relative "generators/instance_sample_generator"
12
+ require_relative "presentation/css_generator"
13
+ require_relative "presentation/javascript_generator"
14
+ require_relative "presentation/navigation_builder"
15
+ require_relative "../parser/xsd_parser"
16
+
17
+ module Xseed
18
+ module Documentation
19
+ # Main HTML documentation generator
20
+ # Integrates all content generators for complete schema documentation
21
+ class HtmlGenerator
22
+ include Utils::References
23
+ include Utils::Namespaces
24
+ include Utils::Helpers
25
+ include Constants
26
+
27
+ attr_reader :xsd_file, :config, :parser
28
+
29
+ # Initialize the HTML generator
30
+ #
31
+ # @param xsd_file [String] Path to XSD schema file
32
+ # @param config [Config] Configuration options (optional)
33
+ def initialize(xsd_file, config = Config.new)
34
+ @xsd_file = xsd_file
35
+ @config = config
36
+ @parser = Parser::XsdParser.new(xsd_file)
37
+ end
38
+
39
+ # Generate HTML documentation
40
+ #
41
+ # @return [String] Generated HTML content
42
+ def generate
43
+ builder = Nokogiri::HTML::Builder.new do |html|
44
+ html.html do
45
+ generate_head(html)
46
+ html.body do
47
+ generate_body(html)
48
+ generate_navigation(html)
49
+ end
50
+ end
51
+ end
52
+
53
+ # Replace HTML4 DOCTYPE with HTML5 DOCTYPE
54
+ html_output = builder.to_html.sub(
55
+ /<!DOCTYPE[^>]+>/,
56
+ "<!DOCTYPE html>",
57
+ )
58
+
59
+ # Convert HTML void hr tags to XML-style (xs3p compliance)
60
+ html_output.gsub!("<hr>", "<hr></hr>")
61
+ end
62
+
63
+ # Generate HTML documentation and write to file
64
+ #
65
+ # @param output_path [String] Path to output HTML file
66
+ def generate_file(output_path)
67
+ # Auto-generate SVG diagrams if enabled
68
+ generate_svg_diagrams(output_path) if @config.print_diagrams
69
+
70
+ html_content = generate
71
+ File.write(output_path, html_content)
72
+ end
73
+
74
+ private
75
+
76
+ # Compactify HTML to match xs3p compact format
77
+ # Removes whitespace between tags while preserving text content
78
+ #
79
+ # @param html [String] HTML content
80
+ # @return [String] Compactified HTML
81
+ def compactify_html(html)
82
+ # Remove only newlines and indentation whitespace between tags
83
+ # This matches xs3p's compact inline format
84
+ html.gsub(/>\n\s*</, "><")
85
+ end
86
+
87
+ # Generate modal popup divs for element/attribute documentation (xs3p pattern)
88
+ #
89
+ # @param html [Nokogiri::HTML::Builder] HTML builder
90
+ def generate_modal_popups(html)
91
+ modal_counter = 0
92
+
93
+ # Helper to generate a single modal
94
+ generate_modal = lambda do |component_name, component_type, doc_text|
95
+ next unless doc_text && !doc_text.strip.empty?
96
+
97
+ modal_counter += 1
98
+ modal_id = "id#{modal_counter}"
99
+
100
+ html.div(class: "modal fade #{component_name}",
101
+ id: "#{modal_id}-popup",
102
+ tabindex: "-1",
103
+ role: "dialog",
104
+ "aria-hidden": "true") do
105
+ html.div(class: "modal-header") do
106
+ html.button(type: "button",
107
+ class: "close",
108
+ "data-dismiss": "modal",
109
+ "aria-hidden": "true") { html.text "×" }
110
+ html.h4(class: "modal-title", id: "#{modal_id}-label") do
111
+ html.text "#{component_type} #{component_name}"
112
+ end
113
+ end
114
+ html.div(class: "modal-body") do
115
+ html.div(class: "annotation documentation",
116
+ id: "wdoc-#{modal_id}-hidden") do
117
+ html.div(class: "hidden", id: "#{modal_id}-hidden-doc-raw") do
118
+ html.text doc_text.strip
119
+ end
120
+ html.div(class: "xs3p-doc", id: "#{modal_id}-hidden-doc") do
121
+ html.text " "
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ # Generate modals for all elements
129
+ parser.elements.each do |element|
130
+ element_name = element["name"]
131
+ next unless element_name
132
+
133
+ doc = extract_documentation(element)
134
+ generate_modal.call(element_name, "Element", doc)
135
+
136
+ # Check for nested elements in inline complexType
137
+ complex_type = element.at_xpath("xs:complexType", "xs" => "http://www.w3.org/2001/XMLSchema")
138
+ if complex_type
139
+ # Get sequence/choice/all children
140
+ %w[sequence choice all].each do |group_type|
141
+ group = complex_type.at_xpath("xs:#{group_type}", "xs" => "http://www.w3.org/2001/XMLSchema")
142
+ next unless group
143
+
144
+ group.xpath(".//xs:element",
145
+ "xs" => "http://www.w3.org/2001/XMLSchema").each do |nested_elem|
146
+ nested_name = nested_elem["name"]
147
+ next unless nested_name
148
+
149
+ nested_doc = extract_documentation(nested_elem)
150
+ generate_modal.call(nested_name, "Element", nested_doc)
151
+ end
152
+
153
+ # Check for attributes
154
+ group.xpath(".//xs:attribute",
155
+ "xs" => "http://www.w3.org/2001/XMLSchema").each do |attr|
156
+ attr_name = attr["name"]
157
+ next unless attr_name
158
+
159
+ attr_doc = extract_documentation(attr)
160
+ generate_modal.call(attr_name, "Attribute", attr_doc)
161
+ end
162
+ end
163
+
164
+ # Direct attributes on complexType
165
+ complex_type.xpath("xs:attribute",
166
+ "xs" => "http://www.w3.org/2001/XMLSchema").each do |attr|
167
+ attr_name = attr["name"]
168
+ next unless attr_name
169
+
170
+ attr_doc = extract_documentation(attr)
171
+ generate_modal.call(attr_name, "Attribute", attr_doc)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Generate modals for complex types
177
+ parser.complex_types.each do |type|
178
+ type_name = type["name"]
179
+ next unless type_name
180
+
181
+ # Check for nested elements
182
+ %w[sequence choice all].each do |group_type|
183
+ group = type.at_xpath(".//xs:#{group_type}", "xs" => "http://www.w3.org/2001/XMLSchema")
184
+ next unless group
185
+
186
+ group.xpath(".//xs:element",
187
+ "xs" => "http://www.w3.org/2001/XMLSchema").each do |nested_elem|
188
+ nested_name = nested_elem["name"]
189
+ next unless nested_name
190
+
191
+ nested_doc = extract_documentation(nested_elem)
192
+ generate_modal.call(nested_name, "Element", nested_doc)
193
+ end
194
+ end
195
+
196
+ # Attributes
197
+ type.xpath(".//xs:attribute",
198
+ "xs" => "http://www.w3.org/2001/XMLSchema").each do |attr|
199
+ attr_name = attr["name"]
200
+ next unless attr_name
201
+
202
+ attr_doc = extract_documentation(attr)
203
+ generate_modal.call(attr_name, "Attribute", attr_doc)
204
+ end
205
+ end
206
+ end
207
+
208
+ # Extract documentation from XSD node
209
+ def extract_documentation(node)
210
+ doc_node = node.at_xpath("xs:annotation/xs:documentation", "xs" => "http://www.w3.org/2001/XMLSchema")
211
+ doc_node&.text
212
+ end
213
+
214
+ # Auto-generate SVG diagrams for all elements using xsdvi
215
+ #
216
+ # @param html_output_path [String] Path to HTML output file
217
+ def generate_svg_diagrams(html_output_path)
218
+ require "xsdvi"
219
+ require "fileutils"
220
+
221
+ # Determine diagrams directory relative to HTML output
222
+ output_dir = File.dirname(html_output_path)
223
+ diagrams_path = File.join(output_dir, @config.diagrams_dir)
224
+ FileUtils.mkdir_p(diagrams_path)
225
+
226
+ # Generate SVG for each element
227
+ parser.elements.each do |element|
228
+ element_name = element["name"]
229
+ next unless element_name
230
+
231
+ svg_file = File.join(diagrams_path, "#{element_name}.svg")
232
+
233
+ # Use xsdvi Ruby API
234
+ writer = Xsdvi::Utils::Writer.new(svg_file)
235
+ builder = Xsdvi::Tree::Builder.new
236
+ handler = Xsdvi::XsdHandler.new(builder)
237
+ handler.root_node_name = element_name
238
+ handler.one_node_only = true
239
+ handler.process_file(@xsd_file)
240
+
241
+ root = builder.root
242
+ generator = Xsdvi::SVG::Generator.new(writer)
243
+ generator.hide_menu_buttons = true
244
+ generator.draw(root)
245
+ end
246
+ rescue LoadError
247
+ # xsdvi not available, skip SVG generation
248
+ warn "Warning: xsdvi gem not available, skipping SVG generation"
249
+ end
250
+
251
+ # Generate HTML head section
252
+ #
253
+ # @param html [Nokogiri::HTML::Builder] HTML builder
254
+ def generate_head(html)
255
+ html.head do
256
+ html.meta(charset: "UTF-8")
257
+ html.meta(name: "viewport",
258
+ content: "width=device-width, initial-scale=1.0")
259
+ html.title(title)
260
+ generate_styles(html)
261
+ generate_scripts(html)
262
+ end
263
+ end
264
+
265
+ # Generate styles (inline or external)
266
+ #
267
+ # @param html [Nokogiri::HTML::Builder] HTML builder
268
+ def generate_styles(html)
269
+ # Load Bootstrap CSS first (needed for modal styling)
270
+ bootstrap_url = @config.bootstrap_url || "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1"
271
+ html.link(rel: "stylesheet",
272
+ href: "#{bootstrap_url}/css/bootstrap.min.css")
273
+
274
+ css_gen = Presentation::CssGenerator.new(@config)
275
+
276
+ if css_gen.external_css_url
277
+ html.link(rel: "stylesheet", href: css_gen.external_css_url)
278
+ else
279
+ html.style do
280
+ html.text(css_gen.generate)
281
+ end
282
+ end
283
+ end
284
+
285
+ # Generate scripts (inline or external)
286
+ #
287
+ # @param html [Nokogiri::HTML::Builder] HTML builder
288
+ def generate_scripts(html)
289
+ js_gen = Presentation::JavascriptGenerator.new(@config)
290
+
291
+ # jQuery
292
+ html.script(src: js_gen.jquery_url, defer: true) {}
293
+
294
+ # Bootstrap JS (if enabled)
295
+ if js_gen.bootstrap_url
296
+ html.script(src: "#{js_gen.bootstrap_url}/js/bootstrap.min.js",
297
+ defer: true) {}
298
+ end
299
+
300
+ # Custom JavaScript
301
+ html.script(defer: true) do
302
+ html.text(js_gen.generate)
303
+ end
304
+ end
305
+
306
+ # Generate navigation sidebar
307
+ #
308
+ # @param html [Nokogiri::HTML::Builder] HTML builder
309
+ def generate_navigation(html)
310
+ nav_builder = Presentation::NavigationBuilder.new(@parser, @config)
311
+ html << nav_builder.generate
312
+ end
313
+
314
+ # Generate HTML body content
315
+ #
316
+ # @param html [Nokogiri::HTML::Builder] HTML builder
317
+ def generate_body(html)
318
+ # Generate modal popups FIRST (xs3p pattern)
319
+ generate_modal_popups(html)
320
+
321
+ # Toggle button as direct child of body
322
+ html.div(id: "toggle") do
323
+ html.span "<"
324
+ end
325
+
326
+ html.main do
327
+ html.div(class: "title-section") do
328
+ html.h1 do
329
+ html.a(id: "top") {}
330
+ html.text title
331
+ end
332
+ end
333
+
334
+ # Schema-level information
335
+ generate_schema_info(html)
336
+
337
+ # Generate documentation with 4 major sections
338
+ generate_component_sections(html)
339
+
340
+ # Glossary section (if enabled)
341
+ generate_glossary(html) if @config.print_glossary
342
+ end
343
+ end
344
+
345
+ # Generate components in 4 major sections (XS3P/XSDVI compliance)
346
+ #
347
+ # @param html [Nokogiri::HTML::Builder] HTML builder
348
+ def generate_component_sections(html)
349
+ # Section 2: Global Elements
350
+ if parser.elements.any?
351
+ html.section(id: "SectionSchemaElements", class: "schema-section") do
352
+ html.h2 do
353
+ html.a(id: "SchemaElements") {}
354
+ html.text "Elements"
355
+ end
356
+ parser.elements.each do |element|
357
+ generate_component_content(html, element, "Element")
358
+ end
359
+ end
360
+ end
361
+
362
+ # Section 3: Complex Types
363
+ complex_types = parser.complex_types
364
+ if complex_types.any?
365
+ html.section(id: "SectionSchemaComplexTypes",
366
+ class: "schema-section") do
367
+ html.h2 do
368
+ html.a(id: "SchemaComplexTypes") {}
369
+ html.text "Complex Types"
370
+ end
371
+ complex_types.each do |type|
372
+ generate_component_content(html, type, "Complex Type")
373
+ end
374
+ end
375
+ end
376
+
377
+ # Section 3b: Simple Types
378
+ simple_types = parser.simple_types
379
+ if simple_types.any?
380
+ html.section(id: "SectionSchemaSimpleTypes",
381
+ class: "schema-section") do
382
+ html.h2 do
383
+ html.a(id: "SchemaSimpleTypes") {}
384
+ html.text "Types"
385
+ end
386
+ simple_types.each do |type|
387
+ generate_component_content(html, type, "Simple Type")
388
+ end
389
+ end
390
+ end
391
+
392
+ # Section 4: Attribute Groups
393
+ attr_groups = parser.attribute_groups
394
+ if attr_groups.any?
395
+ html.section(id: "SectionSchemaAttributeGroups",
396
+ class: "schema-section") do
397
+ html.h2 do
398
+ html.a(id: "SchemaAttributeGroups") {}
399
+ html.text "Attribute Groups"
400
+ end
401
+ attr_groups.each do |component|
402
+ generate_component_content(html, component, "Attribute Group")
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ # Get component type label
409
+ #
410
+ # @param node_name [String] XML node name
411
+ # @return [String] Human-readable component type
412
+ def get_component_type_label(node_name)
413
+ case node_name
414
+ when "element"
415
+ "Element"
416
+ when "complexType"
417
+ "Complex Type"
418
+ when "simpleType"
419
+ "Simple Type"
420
+ when "group"
421
+ "Model Group"
422
+ when "attributeGroup"
423
+ "Attribute Group"
424
+ when "attribute"
425
+ "Attribute"
426
+ when "notation"
427
+ "Notation"
428
+ else
429
+ node_name
430
+ end
431
+ end
432
+
433
+ # Generate glossary section
434
+ #
435
+ # @param html [Nokogiri::HTML::Builder] HTML builder
436
+ def generate_glossary(html)
437
+ html.section(id: "Glossary") do
438
+ html.h2 "Glossary"
439
+ html.p "XSD schema terms and definitions."
440
+ # TODO: Implement glossary generation
441
+ end
442
+ end
443
+
444
+ # Generate schema information section
445
+ #
446
+ # @param html [Nokogiri::HTML::Builder] HTML builder
447
+ def generate_schema_info(html)
448
+ schema = parser.schema
449
+ return unless schema
450
+
451
+ html.section(id: "SectionSchemaProperties", class: "schema-info") do
452
+ html.h2 do
453
+ html.a(id: "SchemaProperties") {}
454
+ html.text "Schema Document Properties"
455
+ end
456
+
457
+ html.dl(class: "dl-horizontal") do
458
+ # Target Namespace
459
+ html.dt(class: "header") do
460
+ html.a(title: "Look up 'Target Namespace' in glossary",
461
+ href: "#term_TargetNS") do
462
+ html.text "Target Namespace"
463
+ end
464
+ end
465
+ html.dd(class: "") do
466
+ if (target_ns = schema["targetNamespace"])
467
+ html.span(class: "targetNS") do
468
+ html.text target_ns
469
+ end
470
+ else
471
+ html.text "None"
472
+ end
473
+ end
474
+
475
+ # Element and Attribute Namespaces
476
+ html.dt(class: "header") do
477
+ html.text "Element and Attribute Namespaces"
478
+ end
479
+ html.dd(class: "") do
480
+ html.ul do
481
+ html.li do
482
+ html.text "Global element and attribute declarations belong to this schema's target namespace."
483
+ end
484
+
485
+ # Check elementFormDefault
486
+ element_form = schema["elementFormDefault"]
487
+ if element_form == "qualified"
488
+ html.li do
489
+ html.text "By default, local element declarations belong to this schema's target namespace."
490
+ end
491
+ else
492
+ html.li do
493
+ html.text "By default, local element declarations have no namespace."
494
+ end
495
+ end
496
+
497
+ html.li do
498
+ html.text "By default, local attribute declarations have no namespace."
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ # Declared Namespaces
505
+ generate_declared_namespaces(html, schema)
506
+
507
+ # Schema Component Representation
508
+ generate_schema_component_callout(html, schema)
509
+
510
+ html.div(style: "text-align: right; clear: both;") do
511
+ html.a(href: "#top", title: "Go to top of page") do
512
+ html.span(class: "glyphicon glyphicon-chevron-up") do
513
+ html.text " "
514
+ end
515
+ end
516
+ end
517
+ html.hr
518
+ end
519
+ end
520
+
521
+ # Generate declared namespaces section
522
+ #
523
+ # @param html [Nokogiri::HTML::Builder] HTML builder
524
+ # @param schema [Nokogiri::XML::Element] Schema element
525
+ def generate_declared_namespaces(html, schema)
526
+ html.h4 "Declared Namespaces:"
527
+ html.dl(class: "dl-horizontal") do
528
+ html.dt(class: "header") { html.text "Prefix" }
529
+ html.dd(class: "header") { html.text "Namespace" }
530
+
531
+ # Extract namespace declarations
532
+ schema.namespace_definitions.each do |ns|
533
+ prefix = ns.prefix || "(default)"
534
+ html.dt(class: "") do
535
+ html.a(id: "ns_#{ns.prefix}") {} if ns.prefix
536
+ html.text prefix
537
+ end
538
+ html.dd(class: "") { html.text ns.href }
539
+ end
540
+ end
541
+ end
542
+
543
+ # Generate component content as direct children (not wrapped in div) per xs3p
544
+ #
545
+ # @param html [Nokogiri::HTML::Builder] HTML builder
546
+ # @param component [Nokogiri::XML::Element] Schema component
547
+ # @param component_type [String] Component type label
548
+ def generate_component_content(html, component, component_type)
549
+ component_name = component["name"] || component["ref"]
550
+ return unless component_name
551
+
552
+ component_id = generate_component_id(component_type, component_name)
553
+
554
+ # XS3P does NOT wrap components in divs - content flows directly
555
+ html.h3(class: "xs3p-subsection-heading") do
556
+ html.text "#{component_type}: "
557
+ html.a(id: component_id) {}
558
+ html.strong component_name
559
+ end
560
+
561
+ # SVG diagram reference (if exists)
562
+ generate_svg_reference(html, component_name)
563
+
564
+ # Properties definition lists (no heading) - generates 1-3 DLs per component
565
+ props_gen = Generators::PropertiesTableGenerator.new(component,
566
+ @config)
567
+ props_gen.generate.each { |dl_html| html << dl_html }
568
+
569
+ # Hierarchy table (if applicable)
570
+ hier_gen = Generators::HierarchyTableGenerator.new(
571
+ component,
572
+ @parser,
573
+ @config,
574
+ )
575
+ hierarchy_html = hier_gen.generate
576
+ html << hierarchy_html if hierarchy_html
577
+
578
+ # Instance sample in callout block (skip for simple types and notations)
579
+ unless %w[simpleType notation].include?(component.name)
580
+ generate_instance_representation_callout(html, component)
581
+ end
582
+
583
+ # Schema component representation in callout block
584
+ generate_schema_component_callout(html, component)
585
+
586
+ # Back to top link and separator
587
+ html.div(style: "text-align: right; clear: both;") do
588
+ html.a(href: "#top", title: "Go to top of page") do
589
+ html.span(class: "glyphicon glyphicon-chevron-up") { html.text " " }
590
+ end
591
+ end
592
+ html.hr
593
+ end
594
+
595
+ # Generate SVG diagram reference
596
+ #
597
+ # @param html [Nokogiri::HTML::Builder] HTML builder
598
+ # @param component_name [String] Component name
599
+ def generate_svg_reference(html, component_name)
600
+ return unless @config.print_diagrams
601
+
602
+ html.object(data: "diagrams/#{component_name}.svg",
603
+ type: "image/svg+xml") {}
604
+ end
605
+
606
+ # Generate instance representation callout block
607
+ #
608
+ # @param html [Nokogiri::HTML::Builder] HTML builder
609
+ # @param component [Nokogiri::XML::Element] Schema component
610
+ def generate_instance_representation_callout(html, component)
611
+ html.div(class: "bs-callout bs-callout-info") do
612
+ html.h4 do
613
+ html.text "XML Instance Representation"
614
+ html.text " "
615
+ generate_help_popover(html, "instance")
616
+ end
617
+ sample_gen = Generators::InstanceSampleGenerator.new(
618
+ component,
619
+ @parser,
620
+ @config,
621
+ )
622
+ html << sample_gen.generate
623
+ end
624
+ end
625
+
626
+ # Generate schema component representation callout block
627
+ #
628
+ # @param html [Nokogiri::HTML::Builder] HTML builder
629
+ # @param component [Nokogiri::XML::Element] Schema component
630
+ def generate_schema_component_callout(html, component)
631
+ html.div(class: "bs-callout bs-callout-info") do
632
+ html.h4 do
633
+ html.text "Schema Component Representation"
634
+ html.text " "
635
+ generate_help_popover(html, "schema")
636
+ end
637
+ html.pre(class: "codehilite") do
638
+ html << format_xsd_component(component)
639
+ end
640
+ end
641
+ end
642
+
643
+ # Generate help popover button
644
+ #
645
+ # @param html [Nokogiri::HTML::Builder] HTML builder
646
+ # @param type [String] Type of help ("instance" or "schema")
647
+ def generate_help_popover(html, type)
648
+ content = if type == "instance"
649
+ instance_help_content
650
+ else
651
+ schema_help_content
652
+ end
653
+
654
+ html.span(class: "xs3p-panel-help") do
655
+ html.button(type: "button",
656
+ class: "btn btn-doc",
657
+ "data-container": "body",
658
+ "data-toggle": "popover",
659
+ "data-placement": type == "instance" ? "right" : "left",
660
+ "data-html": "true",
661
+ "data-content": content) do
662
+ html.span(class: "glyphicon glyphicon-question-sign") do
663
+ html.text " "
664
+ end
665
+ end
666
+ end
667
+ end
668
+
669
+ # Format XSD component for display
670
+ #
671
+ # @param component [Nokogiri::XML::Element] Schema component
672
+ # @return [String] Formatted XSD
673
+ def format_xsd_component(component)
674
+ # Special handling for schema element - collapse children
675
+ if component.name == "schema"
676
+ return format_collapsed_schema(component)
677
+ end
678
+
679
+ # Clone component to avoid modifying original
680
+ comp_copy = component.dup
681
+
682
+ # Remove annotation children (xs3p compliance)
683
+ comp_copy.xpath(".//xsd:annotation", "xsd" => "http://www.w3.org/2001/XMLSchema").each(&:remove)
684
+
685
+ # Get the component's XML representation
686
+ xml = comp_copy.to_xml(indent: 3, indent_text: " ")
687
+
688
+ # Add syntax highlighting classes
689
+ xml.gsub!(/<(\/?)([\w:]+)([^>]*)>/) do
690
+ tag_open = Regexp.last_match(1)
691
+ tag_name = Regexp.last_match(2)
692
+ attributes = Regexp.last_match(3)
693
+
694
+ # Highlight tag names
695
+ highlighted = "<span class=\"nt\">&lt;#{tag_open}"
696
+ if tag_name.include?(":")
697
+ prefix = tag_name.split(":").first
698
+ local_name = tag_name.split(":").last
699
+ highlighted += "<a href=\"#ns_#{prefix}\" title=\"Find out namespace of '#{prefix}' prefix\">#{prefix}</a>:#{local_name}"
700
+ else
701
+ highlighted += tag_name
702
+ end
703
+
704
+ highlighted += "</span>"
705
+
706
+ # Highlight attributes
707
+ if attributes && !attributes.empty?
708
+ attributes.gsub!(/(\w+)="([^"]*)"/) do
709
+ attr_name = Regexp.last_match(1)
710
+ attr_value = Regexp.last_match(2)
711
+
712
+ # Check if attribute value is a type reference
713
+ if attr_name == "type" && !attr_value.include?(":")
714
+ # Local type reference - add link
715
+ attr_value_html = "<span class=\"type\"><a title='Jump to \"#{attr_value}\" type definition.' href=\"#type_#{attr_value}\">#{attr_value}</a></span>"
716
+ " <span class=\"na\">#{attr_name}=</span><span class=\"s\">\"#{attr_value_html}\"</span>"
717
+ elsif attr_name.include?(":") || ["ref",
718
+ "base"].include?(attr_name)
719
+ # Potential reference - add link if local
720
+ local_name = attr_value.include?(":") ? attr_value.split(":").last : attr_value
721
+ if ["ref", "base"].include?(attr_name)
722
+ attr_value_html = "<a title='Jump to \"#{local_name}\" #{attr_name == 'base' ? 'type' : 'element'} definition.' href=\"##{attr_name == 'base' ? 'type' : 'element'}_#{local_name}\">#{attr_value}</a>"
723
+ " <span class=\"na\">#{attr_name}=</span><span class=\"s\">\"#{attr_value_html}\"</span>"
724
+ else
725
+ " <span class=\"na\">#{attr_name}=</span><span class=\"s\">\"#{attr_value}\"</span>"
726
+ end
727
+ else
728
+ " <span class=\"na\">#{attr_name}=</span><span class=\"s\">\"#{attr_value}\"</span>"
729
+ end
730
+ end
731
+ highlighted += attributes
732
+ end
733
+
734
+ highlighted += "<span class=\"nt\">&gt;</span>"
735
+ highlighted
736
+ end
737
+
738
+ xml
739
+ end
740
+
741
+ # Format collapsed schema component (xs3p compliance)
742
+ #
743
+ # @param schema [Nokogiri::XML::Element] Schema element
744
+ # @return [String] Formatted collapsed XSD
745
+ def format_collapsed_schema(schema)
746
+ result = []
747
+
748
+ # Opening tag with attributes
749
+ tag_parts = ["<span class=\"nt\">&lt;"]
750
+ tag_parts << "<a href=\"#ns_xsd\" title=\"Find out namespace of 'xsd' prefix\">xsd</a>:schema</span>"
751
+
752
+ # Add schema attributes
753
+ schema.attributes.each do |name, attr|
754
+ tag_parts << " <span class=\"na\">#{name}=</span><span class=\"s\">\"#{attr.value}\"</span>"
755
+ end
756
+ tag_parts << "<span class=\"nt\">&gt;</span>"
757
+
758
+ result << tag_parts.join
759
+
760
+ # Show first import/include child if exists
761
+ first_child = schema.children.find do |c|
762
+ c.element? && %w[import include].include?(c.name)
763
+ end
764
+ if first_child
765
+ # Format just the first import/include line
766
+ child_line = " <span class=\"nt\">&lt;"
767
+ child_line += "<a href=\"#ns_xsd\" title=\"Find out namespace of 'xsd' prefix\">xsd</a>:#{first_child.name}</span>"
768
+
769
+ first_child.attributes.each do |name, attr|
770
+ child_line += " <span class=\"na\">#{name}=</span><span class=\"s\">\"#{attr.value}\"</span>"
771
+ end
772
+ child_line += "<span class=\"nt\">/&gt;</span>"
773
+
774
+ result << child_line
775
+ end
776
+
777
+ # Collapsed content placeholder
778
+ result << "<span class=\"scContent\">...</span>"
779
+
780
+ # Closing tag
781
+ result << "<span class=\"nt\">&lt;/<a href=\"#ns_xsd\" title=\"Find out namespace of 'xsd' prefix\">xsd</a>:schema&gt;</span>"
782
+
783
+ result.join("\n")
784
+ end
785
+
786
+ # Help content for instance representation
787
+ #
788
+ # @return [String] HTML help content
789
+ def instance_help_content
790
+ "The XML Instance Representation table shows the schema component's content as an XML instance. " \
791
+ "&lt;ul&gt;" \
792
+ "&lt;li&gt;The minimum and maximum occurrence of elements and attributes are provided in square brackets, e.g. [0..1].&lt;/li&gt;" \
793
+ "&lt;li&gt;Model group information are shown in gray, e.g. Start Choice ... End Choice.&lt;/li&gt;" \
794
+ "&lt;li&gt;For type derivations, the elements and attributes that have been added to or changed from the base type's content are shown in &lt;strong&gt;bold&lt;/strong&gt;&lt;/li&gt;" \
795
+ "&lt;li&gt;If an element/attribute has a fixed value, the fixed value is shown in green.&lt;/li&gt;" \
796
+ "&lt;/ul&gt;"
797
+ end
798
+
799
+ # Help content for schema component
800
+ #
801
+ # @return [String] HTML help content
802
+ def schema_help_content
803
+ "The Schema Component Representation table below displays the underlying XML representation of the schema component. (Annotations are not shown.)"
804
+ end
805
+
806
+ # Generate component ID for anchor links
807
+ #
808
+ # @param component_type [String] Component type
809
+ # @param component_name [String] Component name
810
+ # @return [String] Component ID
811
+ def generate_component_id(component_type, component_name)
812
+ case component_type
813
+ when "Element"
814
+ "element-#{component_name}"
815
+ when "Complex Type"
816
+ "type-#{component_name}"
817
+ when "Simple Type"
818
+ "type-#{component_name}"
819
+ when "Model Group"
820
+ "group-#{component_name}"
821
+ when "Attribute Group"
822
+ "attributeGroup-#{component_name}"
823
+ else
824
+ "#{component_type.downcase.tr(' ', '-')}-#{component_name}"
825
+ end
826
+ end
827
+
828
+ # Get the title for the documentation
829
+ #
830
+ # @return [String] Documentation title
831
+ def title
832
+ @config.title || "XSD Schema Documentation"
833
+ end
834
+ end
835
+ end
836
+ end