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,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Xseed
6
+ module Documentation
7
+ module Presentation
8
+ # Generates navigation sidebar (Table of Contents) for HTML documentation
9
+ # Ports functionality from XS3P navigation.xsl
10
+ class NavigationBuilder
11
+ def initialize(parser, config)
12
+ @parser = parser
13
+ @config = config
14
+ end
15
+
16
+ def generate
17
+ builder = Nokogiri::HTML::Builder.new do |html|
18
+ html.nav(id: "toc") do
19
+ html.ul(class: "nav nav-list xs3p-sidenav") do
20
+ generate_toc_items(html)
21
+ end
22
+ end
23
+ end
24
+ builder.to_html
25
+ end
26
+
27
+ private
28
+
29
+ def generate_toc_items(html)
30
+ # Schema properties
31
+ html.li do
32
+ html.strong do
33
+ html.a("Schema Document Properties", href: "#SchemaProperties")
34
+ end
35
+ end
36
+
37
+ # Generate component sections based on sorting preference
38
+ if @config.sort_by_component
39
+ generate_sorted_components(html)
40
+ else
41
+ generate_unsorted_components(html)
42
+ end
43
+
44
+ # Glossary (if enabled)
45
+ return unless @config.print_glossary
46
+
47
+ html.li do
48
+ html.strong do
49
+ html.a("Glossary", href: "#Glossary")
50
+ end
51
+ end
52
+ end
53
+
54
+ def generate_sorted_components(html)
55
+ # Elements
56
+ if @parser.elements.any?
57
+ generate_component_section(html, @parser.elements, "Elements",
58
+ "#SchemaElements")
59
+ end
60
+
61
+ # Complex Types
62
+ if @parser.complex_types.any?
63
+ generate_component_section(html, @parser.complex_types,
64
+ "Complex Types", "#SchemaComplexTypes")
65
+ end
66
+
67
+ # Groups
68
+ if @parser.groups.any?
69
+ generate_component_section(html, @parser.groups, "Groups",
70
+ "#SchemaGroups")
71
+ end
72
+
73
+ # Simple Types
74
+ if @parser.simple_types.any?
75
+ generate_component_section(html, @parser.simple_types, "Types",
76
+ "#SchemaSimpleTypes")
77
+ end
78
+
79
+ # Attribute Groups
80
+ return unless @parser.attribute_groups.any?
81
+
82
+ generate_component_section(html, @parser.attribute_groups,
83
+ "Attribute Groups", "#SchemaAttributeGroups")
84
+ end
85
+
86
+ def generate_unsorted_components(html)
87
+ html.li do
88
+ html.strong do
89
+ html.a("Global Schema Components", href: "#SchemaComponents")
90
+ end
91
+ end
92
+
93
+ # Generate all components in order they appear
94
+ all_components = []
95
+ if @parser.attribute_groups.any?
96
+ all_components.concat(@parser.attribute_groups)
97
+ end
98
+ if @parser.complex_types.any?
99
+ all_components.concat(@parser.complex_types)
100
+ end
101
+ all_components.concat(@parser.elements) if @parser.elements.any?
102
+ all_components.concat(@parser.groups) if @parser.groups.any?
103
+ if @parser.simple_types.any?
104
+ all_components.concat(@parser.simple_types)
105
+ end
106
+
107
+ all_components.each do |component|
108
+ generate_component_link(html, component)
109
+ end
110
+ end
111
+
112
+ def generate_component_section(html, components, title, anchor)
113
+ return if components.empty?
114
+
115
+ html.li do
116
+ html.strong do
117
+ html.a(title, href: anchor)
118
+ end
119
+
120
+ html.ul(class: "nav nav-list nav-list-#{title.downcase.tr(' ',
121
+ '-')}") do
122
+ sorted_components = components.sort_by { |c| c["name"] || "" }
123
+ sorted_components.each do |component|
124
+ generate_component_link(html, component)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def generate_component_link(html, component)
131
+ component_name = component["name"]
132
+ return unless component_name
133
+
134
+ component_type = component.name
135
+ component_id = generate_component_id(component_type, component_name)
136
+
137
+ html.li(class: "nav-sub-item") do
138
+ html.a(href: "##{component_id}") do
139
+ html.strong component_name
140
+ end
141
+ end
142
+ end
143
+
144
+ def generate_component_id(component_type, component_name)
145
+ type_name = case component_type
146
+ when "element"
147
+ "element"
148
+ when "complexType"
149
+ "complex-type"
150
+ when "simpleType"
151
+ "simple-type"
152
+ when "group"
153
+ "model-group"
154
+ when "attributeGroup"
155
+ "attribute-group"
156
+ when "attribute"
157
+ "attribute"
158
+ when "notation"
159
+ "notation"
160
+ else
161
+ component_type.downcase
162
+ end
163
+
164
+ "#{type_name}-#{component_name}"
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/xsd"
4
+ require "nokogiri"
5
+
6
+ module Xseed
7
+ module Documentation
8
+ # Wrapper around Lutaml::Xsd for loading and parsing XSD schemas
9
+ #
10
+ # This class provides a simple interface to lutaml-xsd's schema parsing
11
+ # capabilities, with Xseed-specific error handling and configuration.
12
+ #
13
+ # @example Load a schema from file
14
+ # schema = SchemaLoader.load("path/to/schema.xsd")
15
+ # puts schema.target_namespace
16
+ #
17
+ # @example Parse schema content with mappings
18
+ # schema = SchemaLoader.parse(
19
+ # xsd_content,
20
+ # location: "/path/to/schemas",
21
+ # schema_mappings: [
22
+ # { from: "common.xsd", to: "/local/common.xsd" }
23
+ # ]
24
+ # )
25
+ #
26
+ class SchemaLoader
27
+ # Error raised when schema file cannot be loaded
28
+ class LoadError < StandardError; end
29
+
30
+ # Error raised when schema content cannot be parsed
31
+ class ParseError < StandardError; end
32
+
33
+ # Load an XSD schema from a file
34
+ #
35
+ # @param file_path [String] Path to the XSD file
36
+ # @param config [Config] Optional configuration for schema mappings
37
+ # @return [Lutaml::Xsd::Schema] Parsed schema object
38
+ # @raise [LoadError] if file cannot be read
39
+ # @raise [ParseError] if schema cannot be parsed
40
+ def self.load(file_path, config: nil)
41
+ content = File.read(file_path)
42
+ location = File.dirname(File.expand_path(file_path))
43
+
44
+ parse(content, location: location, config: config)
45
+ rescue Errno::ENOENT
46
+ raise LoadError, "Schema file not found: #{file_path}"
47
+ rescue ParseError => e
48
+ raise e
49
+ rescue StandardError => e
50
+ raise ParseError, "Failed to load schema: #{e.message}"
51
+ end
52
+
53
+ # Parse XSD schema content
54
+ #
55
+ # @param content [String] XSD schema content
56
+ # @param location [String] Base directory or URL for resolving imports
57
+ # @param schema_mappings [Array<Hash>] Schema location mappings
58
+ # @param config [Config] Optional configuration for additional mappings
59
+ # @return [Lutaml::Xsd::Schema] Parsed schema object
60
+ # @raise [ParseError] if schema cannot be parsed
61
+ def self.parse(content, location: nil, schema_mappings: nil, config: nil)
62
+ # Validate that it's actually an XSD schema
63
+ validate_schema_content(content)
64
+
65
+ mappings = build_mappings(schema_mappings, config)
66
+
67
+ # Use lutaml-xsd's parse method
68
+ Lutaml::Xsd.parse(
69
+ content,
70
+ location: location,
71
+ schema_mappings: mappings,
72
+ )
73
+ rescue Nokogiri::XML::SyntaxError => e
74
+ raise ParseError, "Invalid XML syntax: #{e.message}"
75
+ rescue StandardError => e
76
+ raise ParseError, "Failed to parse schema: #{e.message}"
77
+ end
78
+
79
+ # Validate that content is an XSD schema document
80
+ #
81
+ # @param content [String] XML content to validate
82
+ # @raise [ParseError] if not a valid schema
83
+ def self.validate_schema_content(content)
84
+ doc = Nokogiri::XML(content)
85
+ root = doc.root
86
+
87
+ raise ParseError, "Empty or invalid XML document" unless root
88
+
89
+ # Check if root element is xs:schema or xsd:schema
90
+ unless root.name == "schema" &&
91
+ root.namespace&.href&.include?("XMLSchema")
92
+ raise ParseError,
93
+ "Not a valid XSD schema: root element must be xs:schema"
94
+ end
95
+ rescue Nokogiri::XML::SyntaxError => e
96
+ raise ParseError, "Invalid XML syntax: #{e.message}"
97
+ end
98
+
99
+ # Build schema location mappings from various sources
100
+ #
101
+ # @param explicit_mappings [Array<Hash>] Explicitly provided mappings
102
+ # @param config [Config] Configuration object with potential mappings
103
+ # @return [Array<Hash>] Combined schema location mappings
104
+ def self.build_mappings(explicit_mappings, config)
105
+ mappings = []
106
+
107
+ # Add explicit mappings if provided
108
+ mappings.concat(explicit_mappings) if explicit_mappings
109
+
110
+ # Add config-based mappings if available
111
+ if config.respond_to?(:schema_mappings)
112
+ mappings.concat(config.schema_mappings)
113
+ end
114
+
115
+ mappings.empty? ? nil : mappings
116
+ end
117
+
118
+ private_class_method :build_mappings, :validate_schema_content
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Xseed
6
+ module Documentation
7
+ module Utils
8
+ # Helpers utility module for common formatting and component utilities
9
+ #
10
+ # This module provides methods for formatting schema component descriptions,
11
+ # generating anchor IDs, formatting occurrences, URIs, boolean values, and
12
+ # various XSD-specific sets.
13
+ #
14
+ # Based on XS3P common-helpers.xsl utilities.
15
+ module Helpers
16
+ # Component type prefixes for anchor IDs
17
+ COMPONENT_PREFIXES = {
18
+ "attribute" => "attr-",
19
+ "attributeGroup" => "attrgrp-",
20
+ "complexType" => "ctype-",
21
+ "element" => "elem-",
22
+ "group" => "grp-",
23
+ "notation" => "nota-",
24
+ "simpleType" => "stype-",
25
+ "key" => "key-",
26
+ "unique" => "key-",
27
+ }.freeze
28
+
29
+ # Returns a human-readable description for a schema component type
30
+ #
31
+ # @param component_type [String] The component type (e.g., "element", "complexType")
32
+ # @return [String] Human-readable description
33
+ #
34
+ # @example
35
+ # component_description("element") #=> "Element"
36
+ # component_description("complexType") #=> "Complex Type"
37
+ def component_description(component_type)
38
+ case component_type
39
+ when "attribute" then "Attribute"
40
+ when "attributeGroup" then "Attribute Group"
41
+ when "complexType" then "Complex Type"
42
+ when "element" then "Element"
43
+ when "simpleType" then "Simple Type"
44
+ when "group" then "Model Group"
45
+ when "notation" then "Notation"
46
+ when "all" then "All Model Group"
47
+ when "choice" then "Choice Model Group"
48
+ when "sequence" then "Sequence Model Group"
49
+ else "Unknown Component"
50
+ end
51
+ end
52
+
53
+ # Generates a unique anchor ID for a schema component
54
+ #
55
+ # @param component_type [String] The component type
56
+ # @param name [String, nil] The component name
57
+ # @return [String] The generated anchor ID
58
+ #
59
+ # @example
60
+ # generate_component_id("element", "Person") #=> "elem-Person"
61
+ # generate_component_id("schema", nil) #=> "schema"
62
+ def generate_component_id(component_type, name)
63
+ return "schema" if component_type == "schema"
64
+
65
+ prefix = COMPONENT_PREFIXES[component_type] || "comp-"
66
+ "#{prefix}#{name}"
67
+ end
68
+
69
+ # Formats min/max occurrences as [min..max]
70
+ #
71
+ # @param min_occurs [Integer, String, nil] Minimum occurrences (default: 1)
72
+ # @param max_occurs [Integer, String, nil] Maximum occurrences (default: 1)
73
+ # @return [String] Formatted occurrence string
74
+ #
75
+ # @example
76
+ # format_occurs(1, 1) #=> "[1]"
77
+ # format_occurs(0, "unbounded") #=> "[0..*]"
78
+ # format_occurs(1, 5) #=> "[1..5]"
79
+ def format_occurs(min_occurs, max_occurs)
80
+ min = min_occurs.nil? ? 1 : min_occurs.to_i
81
+ max = if max_occurs == "unbounded"
82
+ "*"
83
+ elsif max_occurs.nil?
84
+ 1
85
+ else
86
+ max_occurs.to_i
87
+ end
88
+
89
+ if min == 1 && max == 1
90
+ "[1]"
91
+ else
92
+ "[#{min}..#{max}]"
93
+ end
94
+ end
95
+
96
+ # Formats a URI, creating a clickable link for HTTP/HTTPS URIs
97
+ #
98
+ # @param uri [String, nil] The URI to format
99
+ # @return [String] Formatted URI (HTML link or plain text)
100
+ #
101
+ # @example
102
+ # format_uri("http://example.com")
103
+ # #=> "<a href=\"http://example.com\" title=\"http://example.com\">http://example.com</a>"
104
+ #
105
+ # format_uri("schema.xsd")
106
+ # #=> "schema.xsd"
107
+ def format_uri(uri)
108
+ return "" if uri.nil? || uri.empty?
109
+
110
+ if uri.start_with?("http://", "https://")
111
+ escaped_uri = CGI.escape_html(uri)
112
+ "<a href=\"#{escaped_uri}\" title=\"#{escaped_uri}\">#{escaped_uri}</a>"
113
+ else
114
+ uri
115
+ end
116
+ end
117
+
118
+ # Formats a boolean value as "yes" or "no"
119
+ #
120
+ # @param value [Boolean, String, Integer, nil] The boolean value
121
+ # @return [String] "yes" or "no"
122
+ #
123
+ # @example
124
+ # format_boolean(true) #=> "yes"
125
+ # format_boolean("true") #=> "yes"
126
+ # format_boolean(1) #=> "yes"
127
+ # format_boolean(false) #=> "no"
128
+ # format_boolean(nil) #=> "no"
129
+ def format_boolean(value)
130
+ return "no" if value.nil?
131
+
132
+ normalized = value.to_s.downcase.strip
133
+ if %w[true 1].include?(normalized)
134
+ "yes"
135
+ else
136
+ "no"
137
+ end
138
+ end
139
+
140
+ # Formats a block attribute set, expanding #all
141
+ #
142
+ # @param value [String, nil] The block value
143
+ # @return [String] Formatted block set
144
+ #
145
+ # @example
146
+ # format_block_set("#all")
147
+ # #=> "restriction, extension, substitution"
148
+ #
149
+ # format_block_set("restriction")
150
+ # #=> "restriction"
151
+ def format_block_set(value)
152
+ return "" if value.nil? || value.strip.empty?
153
+
154
+ if value.strip == "#all"
155
+ "restriction, extension, substitution"
156
+ else
157
+ value
158
+ end
159
+ end
160
+
161
+ # Formats a derivation set, expanding #all
162
+ #
163
+ # @param value [String, nil] The derivation value
164
+ # @return [String] Formatted derivation set
165
+ #
166
+ # @example
167
+ # format_derivation_set("#all")
168
+ # #=> "restriction, extension"
169
+ #
170
+ # format_derivation_set("restriction")
171
+ # #=> "restriction"
172
+ def format_derivation_set(value)
173
+ return "" if value.nil? || value.strip.empty?
174
+
175
+ if value.strip == "#all"
176
+ "restriction, extension"
177
+ else
178
+ value
179
+ end
180
+ end
181
+
182
+ # Formats a simple type derivation set, expanding #all
183
+ #
184
+ # @param value [String, nil] The derivation value
185
+ # @return [String] Formatted simple derivation set
186
+ #
187
+ # @example
188
+ # format_simple_derivation_set("#all")
189
+ # #=> "restriction, list, union"
190
+ #
191
+ # format_simple_derivation_set("restriction")
192
+ # #=> "restriction"
193
+ def format_simple_derivation_set(value)
194
+ return "" if value.nil? || value.strip.empty?
195
+
196
+ if value.strip == "#all"
197
+ "restriction, list, union"
198
+ else
199
+ value
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Xseed
6
+ module Documentation
7
+ module Utils
8
+ # Namespaces utility module for handling XSD namespace prefixes
9
+ #
10
+ # This module provides methods for formatting namespace prefixes,
11
+ # generating links to namespace declarations, and collecting namespace
12
+ # information from XML Schemas.
13
+ #
14
+ # Based on XS3P namespaces.xsl utilities.
15
+ module Namespaces
16
+ # Formats a namespace prefix with a trailing colon
17
+ #
18
+ # @param prefix [String, nil] The namespace prefix
19
+ # @return [String] The formatted prefix with colon, or empty string
20
+ #
21
+ # @example
22
+ # format_namespace_prefix("xs") #=> "xs:"
23
+ # format_namespace_prefix("") #=> ""
24
+ # format_namespace_prefix(nil) #=> ""
25
+ def format_namespace_prefix(prefix)
26
+ return "" if prefix.nil? || prefix.strip.empty?
27
+
28
+ "#{prefix}:"
29
+ end
30
+
31
+ # Generates an HTML link to a namespace declaration
32
+ #
33
+ # @param prefix [String, nil] The namespace prefix
34
+ # @param schema [Object] The schema object with namespaces hash
35
+ # @param schema_loc [String] Optional schema location for external refs
36
+ # @return [String] HTML link element or plain text
37
+ #
38
+ # @example
39
+ # namespace_link("xs", schema)
40
+ # #=> "<a href=\"#ns-xs\" title=\"...\">xs</a>"
41
+ def namespace_link(prefix, schema, schema_loc: nil)
42
+ return "" if prefix.nil? || prefix.strip.empty?
43
+
44
+ namespaces = schema&.namespaces || {}
45
+
46
+ if namespaces.key?(prefix)
47
+ # Known namespace - create link to declaration
48
+ href = if schema_loc && schema_loc != "this"
49
+ "#{schema_loc}##{namespace_anchor_id(prefix)}"
50
+ else
51
+ "##{namespace_anchor_id(prefix)}"
52
+ end
53
+
54
+ namespace_uri = namespaces[prefix]
55
+ title = "Find out namespace of '#{prefix}' prefix: #{namespace_uri}"
56
+
57
+ "<a href=\"#{CGI.escape_html(href)}\" " \
58
+ "title=\"#{CGI.escape_html(title)}\">#{CGI.escape_html(prefix)}</a>"
59
+ else
60
+ # Unknown namespace - create warning link
61
+ warning = "Unknown namespace prefix: #{prefix}"
62
+ "<a href=\"javascript:void(0)\" " \
63
+ "onclick=\"alert('#{CGI.escape_html(warning)}')\" " \
64
+ "title=\"#{CGI.escape_html(warning)}\">#{CGI.escape_html(prefix)}</a>"
65
+ end
66
+ end
67
+
68
+ # Formats a namespace prefix with optional link and colon
69
+ #
70
+ # @param prefix [String, nil] The namespace prefix
71
+ # @param schema [Object] The schema object with namespaces hash
72
+ # @param link [Boolean] Whether to generate a link (default: true)
73
+ # @param schema_loc [String] Optional schema location for external refs
74
+ # @return [String] Formatted prefix with colon, optionally linked
75
+ #
76
+ # @example
77
+ # format_namespace_with_prefix("xs", schema)
78
+ # #=> "<a href=\"#ns-xs\" title=\"...\">xs</a>:"
79
+ #
80
+ # format_namespace_with_prefix("xs", schema, link: false)
81
+ # #=> "xs:"
82
+ def format_namespace_with_prefix(prefix, schema, link: true,
83
+ schema_loc: nil)
84
+ return "" if prefix.nil? || prefix.strip.empty?
85
+
86
+ if link
87
+ "#{namespace_link(prefix, schema, schema_loc: schema_loc)}:"
88
+ else
89
+ format_namespace_prefix(prefix)
90
+ end
91
+ end
92
+
93
+ # Collects all namespaces from a schema
94
+ #
95
+ # @param schema [Object, nil] The schema object with namespaces hash
96
+ # @return [Hash] Hash of prefix => namespace URI mappings
97
+ #
98
+ # @example
99
+ # collect_namespaces(schema)
100
+ # #=> { "xs" => "http://www.w3.org/2001/XMLSchema",
101
+ # # "tns" => "http://example.com/target" }
102
+ def collect_namespaces(schema)
103
+ return {} if schema.nil?
104
+
105
+ namespaces = schema.namespaces
106
+ return {} if namespaces.nil?
107
+
108
+ namespaces.dup
109
+ end
110
+
111
+ # Generates an anchor ID for a namespace prefix
112
+ #
113
+ # @param prefix [String, nil] The namespace prefix
114
+ # @return [String] The anchor ID for the namespace
115
+ #
116
+ # @example
117
+ # namespace_anchor_id("xs") #=> "ns-xs"
118
+ # namespace_anchor_id("tns") #=> "ns-tns"
119
+ def namespace_anchor_id(prefix)
120
+ "ns-#{prefix}"
121
+ end
122
+
123
+ # Formats a namespace declaration (xmlns)
124
+ #
125
+ # @param prefix [String, nil] The namespace prefix (empty for default)
126
+ # @param namespace [String, nil] The namespace URI
127
+ # @return [String] Formatted xmlns declaration
128
+ #
129
+ # @example
130
+ # format_namespace_declaration("xs", "http://www.w3.org/2001/XMLSchema")
131
+ # #=> "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
132
+ #
133
+ # format_namespace_declaration("", "http://example.com/default")
134
+ # #=> "xmlns=\"http://example.com/default\""
135
+ def format_namespace_declaration(prefix, namespace)
136
+ prefix_part = if prefix.nil? || prefix.strip.empty?
137
+ "xmlns"
138
+ else
139
+ "xmlns:#{prefix}"
140
+ end
141
+
142
+ namespace_value = namespace.nil? ? "" : CGI.escape_html(namespace)
143
+
144
+ "#{prefix_part}=\"#{namespace_value}\""
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end