rng 0.1.1
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.
- checksums.yaml +7 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +23 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Gemfile +16 -0
- data/Rakefile +12 -0
- data/lib/rng/attribute.rb +13 -0
- data/lib/rng/builder.rb +158 -0
- data/lib/rng/define.rb +14 -0
- data/lib/rng/element.rb +26 -0
- data/lib/rng/rnc_parser.rb +136 -0
- data/lib/rng/rng_parser.rb +107 -0
- data/lib/rng/schema.rb +18 -0
- data/lib/rng/start.rb +14 -0
- data/lib/rng/version.rb +5 -0
- data/lib/rng.rb +12 -0
- data/rng.gemspec +38 -0
- data/sig/rng.rbs +4 -0
- data/spec/rng/rnc_parser_spec.rb +66 -0
- data/spec/rng/rng_parser_spec.rb +102 -0
- data/spec/rng/schema_spec.rb +193 -0
- data/spec/rng_spec.rb +7 -0
- data/spec/spec_helper.rb +23 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3b70e1b28d17dd9d6a287a404f218151f5a25ab38eba65aba55330d1e5692a08
|
4
|
+
data.tar.gz: d8524a2367eb0c73b64f9040555a2269ad2e6f8121a5ed5d05021c20ad00042b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 06bc5ef5f3d56f356730e71b42ee2080ab449ed1c769446a315a8077a015327bea94635e172ab9dcbd1d2559eacbb21ad519f3c5dc48fc45c5c7c7ee577326e2
|
7
|
+
data.tar.gz: 2eca01084c177a824f02c5f30c03fc2463de4fee55fab370c9fc951bc51cbf56fadebee4f326c1472754f394361a4e932979d269bf130b1a23312af20feb6483
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Auto-generated by Cimas: Do not edit it manually!
|
2
|
+
# See https://github.com/metanorma/cimas
|
3
|
+
name: rake
|
4
|
+
|
5
|
+
on:
|
6
|
+
push:
|
7
|
+
branches: [ master, main ]
|
8
|
+
tags: [ v* ]
|
9
|
+
pull_request:
|
10
|
+
|
11
|
+
jobs:
|
12
|
+
rake:
|
13
|
+
uses: metanorma/ci/.github/workflows/generic-rake.yml@main
|
14
|
+
secrets:
|
15
|
+
pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Auto-generated by Cimas: Do not edit it manually!
|
2
|
+
# See https://github.com/metanorma/cimas
|
3
|
+
name: release
|
4
|
+
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
inputs:
|
8
|
+
next_version:
|
9
|
+
description: |
|
10
|
+
Next release version. Possible values: x.y.z, major, minor, patch or pre|rc|etc
|
11
|
+
required: true
|
12
|
+
default: 'skip'
|
13
|
+
repository_dispatch:
|
14
|
+
types: [ do-release ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
release:
|
18
|
+
uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
|
19
|
+
with:
|
20
|
+
next_version: ${{ github.event.inputs.next_version }}
|
21
|
+
secrets:
|
22
|
+
rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
|
23
|
+
pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in rng.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "equivalent-xml"
|
9
|
+
gem "nokogiri"
|
10
|
+
gem "xml-c14n"
|
11
|
+
gem "rake", "~> 13.0"
|
12
|
+
gem "rspec", "~> 3.0"
|
13
|
+
gem "rubocop", "~> 1.21"
|
14
|
+
gem "rubocop-performance", require: false
|
15
|
+
gem "rubocop-rake", require: false
|
16
|
+
gem "rubocop-rspec", require: false
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "lutaml/model"
|
2
|
+
|
3
|
+
module Rng
|
4
|
+
class Attribute < Lutaml::Model::Serializable
|
5
|
+
attribute :name, :string
|
6
|
+
attribute :type, :string, collection: true
|
7
|
+
|
8
|
+
xml do
|
9
|
+
map_attribute "name", to: :name
|
10
|
+
map_element "data", to: :type
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/rng/builder.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
module Rng
|
2
|
+
class Builder
|
3
|
+
def build(schema, format:)
|
4
|
+
case format
|
5
|
+
when :rng
|
6
|
+
build_rng(schema)
|
7
|
+
when :rnc
|
8
|
+
build_rnc(schema)
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_rng(schema)
|
17
|
+
doc = Nokogiri::XML::Document.new
|
18
|
+
doc.encoding = "UTF-8"
|
19
|
+
|
20
|
+
if schema.is_a?(Rng::Schema)
|
21
|
+
root = Nokogiri::XML::Node.new("grammar", doc)
|
22
|
+
root["xmlns"] = "http://relaxng.org/ns/structure/1.0"
|
23
|
+
doc.root = root
|
24
|
+
|
25
|
+
start = Nokogiri::XML::Node.new("start", doc)
|
26
|
+
root.add_child(start)
|
27
|
+
|
28
|
+
if schema.start.ref
|
29
|
+
ref = Nokogiri::XML::Node.new("ref", doc)
|
30
|
+
ref["name"] = schema.start.ref
|
31
|
+
start.add_child(ref)
|
32
|
+
end
|
33
|
+
|
34
|
+
schema.start.elements.each do |element|
|
35
|
+
start.add_child(build_rng_element(element, doc))
|
36
|
+
end
|
37
|
+
|
38
|
+
schema.define&.each do |define|
|
39
|
+
define_node = Nokogiri::XML::Node.new("define", doc)
|
40
|
+
define_node["name"] = define.name
|
41
|
+
define.elements.each do |element|
|
42
|
+
define_node.add_child(build_rng_element(element, doc))
|
43
|
+
end
|
44
|
+
root.add_child(define_node)
|
45
|
+
end
|
46
|
+
elsif schema.is_a?(Rng::Element)
|
47
|
+
el = build_rng_element(schema, doc)
|
48
|
+
el["xmlns"] = "http://relaxng.org/ns/structure/1.0"
|
49
|
+
doc.root = el
|
50
|
+
end
|
51
|
+
|
52
|
+
doc.to_xml
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_rng_element(element, doc)
|
56
|
+
if element.zero_or_more&.any?
|
57
|
+
zero_or_more = Nokogiri::XML::Node.new("zeroOrMore", doc)
|
58
|
+
el = Nokogiri::XML::Node.new("element", doc)
|
59
|
+
el["name"] = element.name
|
60
|
+
add_element_content(element, el, doc)
|
61
|
+
zero_or_more.add_child(el)
|
62
|
+
return zero_or_more
|
63
|
+
elsif element.one_or_more&.any?
|
64
|
+
one_or_more = Nokogiri::XML::Node.new("oneOrMore", doc)
|
65
|
+
el = Nokogiri::XML::Node.new("element", doc)
|
66
|
+
el["name"] = element.name
|
67
|
+
add_element_content(element, el, doc)
|
68
|
+
one_or_more.add_child(el)
|
69
|
+
return one_or_more
|
70
|
+
elsif element.optional&.any?
|
71
|
+
optional = Nokogiri::XML::Node.new("optional", doc)
|
72
|
+
el = Nokogiri::XML::Node.new("element", doc)
|
73
|
+
el["name"] = element.name
|
74
|
+
add_element_content(element, el, doc)
|
75
|
+
optional.add_child(el)
|
76
|
+
return optional
|
77
|
+
else
|
78
|
+
el = Nokogiri::XML::Node.new("element", doc)
|
79
|
+
el["name"] = element.name
|
80
|
+
add_element_content(element, el, doc)
|
81
|
+
return el
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_element_content(element, el, doc)
|
86
|
+
element.attributes&.each do |attr|
|
87
|
+
attr_node = Nokogiri::XML::Node.new("attribute", doc)
|
88
|
+
attr_node["name"] = attr.name
|
89
|
+
if attr.type&.any?
|
90
|
+
data = Nokogiri::XML::Node.new("data", doc)
|
91
|
+
data["type"] = attr.type.first
|
92
|
+
attr_node.add_child(data)
|
93
|
+
else
|
94
|
+
text = Nokogiri::XML::Node.new("text", doc)
|
95
|
+
attr_node.add_child(text)
|
96
|
+
end
|
97
|
+
el.add_child(attr_node)
|
98
|
+
end
|
99
|
+
|
100
|
+
element.elements&.each do |child|
|
101
|
+
el.add_child(build_rng_element(child, doc))
|
102
|
+
end
|
103
|
+
|
104
|
+
if element.text
|
105
|
+
text = Nokogiri::XML::Node.new("text", doc)
|
106
|
+
el.add_child(text)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_rnc(schema)
|
111
|
+
result = ""
|
112
|
+
elements = schema.is_a?(Rng::Schema) ? schema.start.elements : [schema]
|
113
|
+
elements.each do |element|
|
114
|
+
result += build_rnc_element(element)
|
115
|
+
end
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_rnc_element(element, indent = 0)
|
120
|
+
return "" unless element # Handle nil elements
|
121
|
+
|
122
|
+
result = " " * indent
|
123
|
+
result += "element #{element.name} {\n"
|
124
|
+
|
125
|
+
element.attributes&.each do |attr|
|
126
|
+
result += " " * (indent + 1)
|
127
|
+
result += "attribute #{attr.name} { text }"
|
128
|
+
result += ",\n" unless element.attributes.last == attr && !element.elements&.any? && !element.text
|
129
|
+
end
|
130
|
+
|
131
|
+
element.elements&.each_with_index do |child, index|
|
132
|
+
child_result = build_rnc_element(child, indent + 1)
|
133
|
+
result += child_result
|
134
|
+
result += "," unless index == element.elements.size - 1 && !element.text
|
135
|
+
result += "\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
if element.text
|
139
|
+
result += " " * (indent + 1)
|
140
|
+
result += "text"
|
141
|
+
result += "\n"
|
142
|
+
end
|
143
|
+
|
144
|
+
result += " " * indent
|
145
|
+
result += "}"
|
146
|
+
|
147
|
+
if element.zero_or_more&.any?
|
148
|
+
result += "*"
|
149
|
+
elsif element.one_or_more&.any?
|
150
|
+
result += "+"
|
151
|
+
elsif element.optional&.any?
|
152
|
+
result += "?"
|
153
|
+
end
|
154
|
+
|
155
|
+
result
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/lib/rng/define.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "lutaml/model"
|
2
|
+
require_relative "element"
|
3
|
+
|
4
|
+
module Rng
|
5
|
+
class Define < Lutaml::Model::Serializable
|
6
|
+
attribute :name, :string
|
7
|
+
attribute :elements, Element, collection: true
|
8
|
+
|
9
|
+
xml do
|
10
|
+
map_attribute "name", to: :name
|
11
|
+
map_element "element", to: :elements
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/rng/element.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "lutaml/model"
|
2
|
+
require_relative "attribute"
|
3
|
+
|
4
|
+
module Rng
|
5
|
+
class Element < Lutaml::Model::Serializable
|
6
|
+
attribute :name, :string
|
7
|
+
attribute :attributes, Attribute, collection: true
|
8
|
+
attribute :elements, Element, collection: true
|
9
|
+
attribute :text, :boolean
|
10
|
+
attribute :zero_or_more, Element, collection: true
|
11
|
+
attribute :one_or_more, Element, collection: true
|
12
|
+
attribute :optional, Element, collection: true
|
13
|
+
attribute :choice, Element, collection: true
|
14
|
+
|
15
|
+
xml do
|
16
|
+
map_attribute "name", to: :name
|
17
|
+
map_element "attribute", to: :attributes
|
18
|
+
map_element "element", to: :elements
|
19
|
+
map_element "text", to: :text
|
20
|
+
map_element "zeroOrMore", to: :zero_or_more
|
21
|
+
map_element "oneOrMore", to: :one_or_more
|
22
|
+
map_element "optional", to: :optional
|
23
|
+
map_element "choice", to: :choice
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require "parslet"
|
2
|
+
require_relative "schema"
|
3
|
+
|
4
|
+
module Rng
|
5
|
+
class RncParser < Parslet::Parser
|
6
|
+
rule(:space) { match('\s').repeat(1) }
|
7
|
+
rule(:space?) { space.maybe }
|
8
|
+
rule(:newline) { (str("\r").maybe >> str("\n")).repeat(1) }
|
9
|
+
rule(:newline?) { newline.maybe }
|
10
|
+
rule(:whitespace) { (space | newline).repeat }
|
11
|
+
rule(:comma) { str(",") }
|
12
|
+
rule(:comma?) { (whitespace >> comma >> whitespace).maybe }
|
13
|
+
|
14
|
+
rule(:identifier) { match("[a-zA-Z0-9_]").repeat(1).as(:identifier) }
|
15
|
+
|
16
|
+
rule(:element_def) {
|
17
|
+
str("element") >> space >>
|
18
|
+
identifier >>
|
19
|
+
whitespace >>
|
20
|
+
str("{") >>
|
21
|
+
whitespace >>
|
22
|
+
content.maybe.as(:content) >>
|
23
|
+
whitespace >>
|
24
|
+
str("}") >>
|
25
|
+
(str("*") | str("+") | str("?")).maybe.as(:occurrence)
|
26
|
+
}
|
27
|
+
|
28
|
+
rule(:attribute_def) {
|
29
|
+
str("attribute") >> space >>
|
30
|
+
identifier.as(:name) >>
|
31
|
+
whitespace >>
|
32
|
+
str("{") >>
|
33
|
+
whitespace >>
|
34
|
+
(str("text")).as(:type) >>
|
35
|
+
whitespace >>
|
36
|
+
str("}")
|
37
|
+
}
|
38
|
+
|
39
|
+
rule(:text_def) { str("text").as(:text) }
|
40
|
+
|
41
|
+
rule(:content_item) {
|
42
|
+
((element_def | attribute_def | text_def).as(:item) >> comma?).repeat(1).as(:items)
|
43
|
+
}
|
44
|
+
|
45
|
+
rule(:content) { content_item }
|
46
|
+
|
47
|
+
rule(:grammar) { whitespace >> element_def.as(:element) >> whitespace }
|
48
|
+
|
49
|
+
root(:grammar)
|
50
|
+
|
51
|
+
def parse(input)
|
52
|
+
tree = super(input.strip)
|
53
|
+
build_schema(tree)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def build_schema(tree)
|
59
|
+
element = tree[:element]
|
60
|
+
Schema.new(
|
61
|
+
start: Start.new(
|
62
|
+
elements: [build_element(element)],
|
63
|
+
),
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_element(element)
|
68
|
+
name = element[:identifier].to_s
|
69
|
+
content = element[:content]&.[](:items)
|
70
|
+
occurrence = element[:occurrence]
|
71
|
+
|
72
|
+
# Create base element
|
73
|
+
el = Element.new(
|
74
|
+
name: name,
|
75
|
+
attributes: [],
|
76
|
+
elements: [],
|
77
|
+
text: false,
|
78
|
+
)
|
79
|
+
|
80
|
+
if content
|
81
|
+
current_elements = []
|
82
|
+
current_attributes = []
|
83
|
+
|
84
|
+
content.each do |item|
|
85
|
+
case
|
86
|
+
when item[:item][:name] || (item[:item][:identifier] && item[:item][:type])
|
87
|
+
attr_name = item[:item][:name] || item[:item][:identifier]
|
88
|
+
attr = Attribute.new(
|
89
|
+
name: attr_name.to_s,
|
90
|
+
type: ["string"],
|
91
|
+
)
|
92
|
+
current_attributes << attr
|
93
|
+
when item[:item][:identifier]
|
94
|
+
current_elements << build_element(item[:item])
|
95
|
+
when item[:item][:text]
|
96
|
+
el.text = true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
el.attributes = current_attributes
|
101
|
+
el.elements = current_elements
|
102
|
+
end
|
103
|
+
|
104
|
+
# Handle occurrence modifiers
|
105
|
+
result = el
|
106
|
+
case occurrence
|
107
|
+
when "*"
|
108
|
+
result = Element.new(
|
109
|
+
name: el.name,
|
110
|
+
attributes: el.attributes,
|
111
|
+
elements: el.elements,
|
112
|
+
text: el.text,
|
113
|
+
)
|
114
|
+
result.zero_or_more = [el]
|
115
|
+
when "+"
|
116
|
+
result = Element.new(
|
117
|
+
name: el.name,
|
118
|
+
attributes: el.attributes,
|
119
|
+
elements: el.elements,
|
120
|
+
text: el.text,
|
121
|
+
)
|
122
|
+
result.one_or_more = [el]
|
123
|
+
when "?"
|
124
|
+
result = Element.new(
|
125
|
+
name: el.name,
|
126
|
+
attributes: el.attributes,
|
127
|
+
elements: el.elements,
|
128
|
+
text: el.text,
|
129
|
+
)
|
130
|
+
result.optional = [el]
|
131
|
+
end
|
132
|
+
|
133
|
+
result
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require_relative "schema"
|
3
|
+
require_relative "element"
|
4
|
+
|
5
|
+
module Rng
|
6
|
+
class RngParser
|
7
|
+
RELAXNG_NS = "http://relaxng.org/ns/structure/1.0"
|
8
|
+
|
9
|
+
def parse(input)
|
10
|
+
doc = Nokogiri::XML(input)
|
11
|
+
doc.remove_namespaces! # This simplifies namespace handling
|
12
|
+
|
13
|
+
root = doc.root
|
14
|
+
case root.name
|
15
|
+
when "grammar"
|
16
|
+
parse_grammar(doc)
|
17
|
+
when "element"
|
18
|
+
parse_element(doc)
|
19
|
+
else
|
20
|
+
raise Rng::Error, "Unexpected root element: #{root.name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parse_grammar(doc)
|
27
|
+
Schema.new(
|
28
|
+
start: parse_start(doc.at_xpath("//start")),
|
29
|
+
define: doc.xpath("//define").map { |define| parse_define(define) },
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_start(node)
|
34
|
+
return nil unless node
|
35
|
+
|
36
|
+
Start.new(
|
37
|
+
ref: node.at_xpath(".//ref")&.attr("name"),
|
38
|
+
elements: node.xpath(".//element").map { |element| parse_element(element) },
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_define(node)
|
43
|
+
Define.new(
|
44
|
+
name: node["name"],
|
45
|
+
elements: node.xpath(".//element").map { |element| parse_element(element) },
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_element(node)
|
50
|
+
return nil unless node["name"]
|
51
|
+
|
52
|
+
element = Element.new(
|
53
|
+
name: node["name"],
|
54
|
+
attributes: [],
|
55
|
+
elements: [],
|
56
|
+
text: false,
|
57
|
+
)
|
58
|
+
|
59
|
+
node.children.each do |child|
|
60
|
+
parse_child(child, element)
|
61
|
+
end
|
62
|
+
|
63
|
+
element
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_child(node, element)
|
67
|
+
case node.name
|
68
|
+
when "attribute"
|
69
|
+
element.attributes << parse_attribute(node)
|
70
|
+
when "element"
|
71
|
+
element.elements << parse_element(node)
|
72
|
+
when "text"
|
73
|
+
element.text = true
|
74
|
+
when "zeroOrMore"
|
75
|
+
parse_zero_or_more(node).each { |el| element.zero_or_more << el }
|
76
|
+
when "oneOrMore"
|
77
|
+
parse_one_or_more(node).each { |el| element.one_or_more << el }
|
78
|
+
when "optional"
|
79
|
+
parse_optional(node).each { |el| element.optional << el }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_attribute(node)
|
84
|
+
data_node = node.at_xpath(".//data")
|
85
|
+
Attribute.new(
|
86
|
+
name: node["name"],
|
87
|
+
type: data_node ? [data_node["type"]] : ["string"],
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_zero_or_more(node)
|
92
|
+
node.xpath("./element").map { |el| parse_element(el) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_one_or_more(node)
|
96
|
+
node.xpath("./element").map { |el| parse_element(el) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_optional(node)
|
100
|
+
node.xpath("./element").map { |el| parse_element(el) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_choice(node)
|
104
|
+
node.xpath(".//choice/element").map { |el| parse_element(el) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/rng/schema.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "lutaml/model"
|
2
|
+
require_relative "start"
|
3
|
+
require_relative "define"
|
4
|
+
|
5
|
+
module Rng
|
6
|
+
class Schema < Lutaml::Model::Serializable
|
7
|
+
attribute :start, Start
|
8
|
+
attribute :define, Define, collection: true
|
9
|
+
|
10
|
+
xml do
|
11
|
+
root "grammar"
|
12
|
+
namespace "http://relaxng.org/ns/structure/1.0"
|
13
|
+
|
14
|
+
map_element "start", to: :start
|
15
|
+
map_element "define", to: :define
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/rng/start.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "lutaml/model"
|
2
|
+
require_relative "element"
|
3
|
+
|
4
|
+
module Rng
|
5
|
+
class Start < Lutaml::Model::Serializable
|
6
|
+
attribute :ref, :string
|
7
|
+
attribute :elements, Element, collection: true
|
8
|
+
|
9
|
+
xml do
|
10
|
+
map_attribute "ref", to: :ref
|
11
|
+
map_element "element", to: :elements
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/rng/version.rb
ADDED
data/lib/rng.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rng/version"
|
4
|
+
require_relative "rng/rnc_parser"
|
5
|
+
require_relative "rng/rng_parser"
|
6
|
+
require_relative "rng/builder"
|
7
|
+
|
8
|
+
module Rng
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
data/rng.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/rng/version"
|
4
|
+
|
5
|
+
all_files_in_git = Dir.chdir(File.expand_path(__dir__)) do
|
6
|
+
`git ls-files -z`.split("\x0")
|
7
|
+
end
|
8
|
+
|
9
|
+
Gem::Specification.new do |spec|
|
10
|
+
spec.name = "rng"
|
11
|
+
spec.version = Rng::VERSION
|
12
|
+
spec.authors = ["Ribose"]
|
13
|
+
spec.email = ["open.source@ribose.com"]
|
14
|
+
|
15
|
+
spec.summary = "Library to parse and build RELAX NG (RNG) and RELAX NG Compact Syntax (RNC) schemas."
|
16
|
+
spec.homepage = "https://github.com/lutaml/rng"
|
17
|
+
spec.license = "BSD-2-Clause"
|
18
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
19
|
+
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
22
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
spec.files = all_files_in_git
|
26
|
+
.reject { |f| f.match(%r{\A(?:test|features|bin|\.)/}) }
|
27
|
+
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "lutaml-model"
|
33
|
+
spec.add_dependency "parslet"
|
34
|
+
spec.add_dependency "nokogiri"
|
35
|
+
|
36
|
+
# spec.add_dependency "thor"
|
37
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
38
|
+
end
|
data/sig/rng.rbs
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Rng::RncParser do
|
4
|
+
let(:parser) { described_class.new }
|
5
|
+
|
6
|
+
describe "#parse" do
|
7
|
+
context "with a simple RNC schema" do
|
8
|
+
let(:input) do
|
9
|
+
<<~RNC
|
10
|
+
element addressBook {
|
11
|
+
element card {
|
12
|
+
element name { text },
|
13
|
+
element email { text }
|
14
|
+
}*
|
15
|
+
}
|
16
|
+
RNC
|
17
|
+
end
|
18
|
+
|
19
|
+
it "correctly parses the schema" do
|
20
|
+
result = parser.parse(input)
|
21
|
+
expect(result).to be_a(Rng::Schema)
|
22
|
+
expect(result.start.elements.first.name).to eq("addressBook")
|
23
|
+
expect(result.start.elements.first.elements.first.name).to eq("card")
|
24
|
+
expect(result.start.elements.first.elements.first.elements.map(&:name)).to eq(["name", "email"])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with attributes" do
|
29
|
+
let(:input) do
|
30
|
+
<<~RNC
|
31
|
+
element person {
|
32
|
+
attribute id { text },
|
33
|
+
element name { text }
|
34
|
+
}
|
35
|
+
RNC
|
36
|
+
end
|
37
|
+
|
38
|
+
it "correctly parses attributes" do
|
39
|
+
result = parser.parse(input)
|
40
|
+
expect(result.start.elements.first.name).to eq("person")
|
41
|
+
expect(result.start.elements.first.elements.first).to be_a(Rng::Attribute)
|
42
|
+
expect(result.start.elements.first.elements.first.name).to eq("id")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with nested elements" do
|
47
|
+
let(:input) do
|
48
|
+
<<~RNC
|
49
|
+
element root {
|
50
|
+
element child1 {
|
51
|
+
element grandchild { text }
|
52
|
+
},
|
53
|
+
element child2 { text }
|
54
|
+
}
|
55
|
+
RNC
|
56
|
+
end
|
57
|
+
|
58
|
+
it "correctly parses nested elements" do
|
59
|
+
result = parser.parse(input)
|
60
|
+
expect(result.start.elements.first.name).to eq("root")
|
61
|
+
expect(result.start.elements.first.elements.map(&:name)).to eq(["child1", "child2"])
|
62
|
+
expect(result.start.elements.first.elements.first.elements.first.name).to eq("grandchild")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Rng::RngParser do
|
4
|
+
let(:parser) { described_class.new }
|
5
|
+
|
6
|
+
describe "#parse" do
|
7
|
+
context "with a simple RNG schema" do
|
8
|
+
let(:input) do
|
9
|
+
<<~RNG
|
10
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
11
|
+
<start>
|
12
|
+
<element name="addressBook">
|
13
|
+
<zeroOrMore>
|
14
|
+
<element name="card">
|
15
|
+
<element name="name">
|
16
|
+
<text/>
|
17
|
+
</element>
|
18
|
+
<element name="email">
|
19
|
+
<text/>
|
20
|
+
</element>
|
21
|
+
</element>
|
22
|
+
</zeroOrMore>
|
23
|
+
</element>
|
24
|
+
</start>
|
25
|
+
</grammar>
|
26
|
+
RNG
|
27
|
+
end
|
28
|
+
|
29
|
+
it "correctly parses the schema" do
|
30
|
+
result = parser.parse(input)
|
31
|
+
expect(result).to be_a(Rng::Schema)
|
32
|
+
expect(result.start.elements.first.name).to eq("addressBook")
|
33
|
+
expect(result.start.elements.first.zero_or_more.first.name).to eq("card")
|
34
|
+
expect(result.start.elements.first.zero_or_more.first.elements.map(&:name)).to eq(["name", "email"])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a complex RNG schema" do
|
39
|
+
let(:input) do
|
40
|
+
<<~RNG
|
41
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
42
|
+
<start>
|
43
|
+
<ref name="addressBook"/>
|
44
|
+
</start>
|
45
|
+
<define name="addressBook">
|
46
|
+
<element name="addressBook">
|
47
|
+
<zeroOrMore>
|
48
|
+
<element name="card">
|
49
|
+
<element name="name">
|
50
|
+
<text/>
|
51
|
+
</element>
|
52
|
+
<element name="email">
|
53
|
+
<text/>
|
54
|
+
</element>
|
55
|
+
<optional>
|
56
|
+
<element name="note">
|
57
|
+
<text/>
|
58
|
+
</element>
|
59
|
+
</optional>
|
60
|
+
</element>
|
61
|
+
</zeroOrMore>
|
62
|
+
</element>
|
63
|
+
</define>
|
64
|
+
</grammar>
|
65
|
+
RNG
|
66
|
+
end
|
67
|
+
|
68
|
+
it "correctly parses the schema" do
|
69
|
+
result = parser.parse(input)
|
70
|
+
expect(result).to be_a(Rng::Schema)
|
71
|
+
expect(result.start.ref).to eq("addressBook")
|
72
|
+
expect(result.define.first.name).to eq("addressBook")
|
73
|
+
expect(result.define.first.elements.first.name).to eq("addressBook")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "with attributes" do
|
78
|
+
let(:input) do
|
79
|
+
<<~RNG
|
80
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
81
|
+
<start>
|
82
|
+
<element name="person">
|
83
|
+
<attribute name="id">
|
84
|
+
<data type="ID"/>
|
85
|
+
</attribute>
|
86
|
+
<element name="name">
|
87
|
+
<text/>
|
88
|
+
</element>
|
89
|
+
</element>
|
90
|
+
</start>
|
91
|
+
</grammar>
|
92
|
+
RNG
|
93
|
+
end
|
94
|
+
|
95
|
+
it "correctly parses attributes" do
|
96
|
+
result = parser.parse(input)
|
97
|
+
expect(result.start.elements.first.attributes.first.name).to eq("id")
|
98
|
+
expect(result.start.elements.first.attributes.first.type).to eq(["ID"])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Rng::Schema do
|
4
|
+
let(:rng_parser) { Rng::RngParser.new }
|
5
|
+
let(:rnc_parser) { Rng::RncParser.new }
|
6
|
+
let(:builder) { Rng::Builder.new }
|
7
|
+
|
8
|
+
describe "RNG parsing and building" do
|
9
|
+
let(:rng_input) do
|
10
|
+
<<~RNG
|
11
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
12
|
+
<element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
|
13
|
+
<zeroOrMore>
|
14
|
+
<element name="card">
|
15
|
+
<element name="name">
|
16
|
+
<text/>
|
17
|
+
</element>
|
18
|
+
<element name="email">
|
19
|
+
<text/>
|
20
|
+
</element>
|
21
|
+
<optional>
|
22
|
+
<element name="note">
|
23
|
+
<text/>
|
24
|
+
</element>
|
25
|
+
</optional>
|
26
|
+
</element>
|
27
|
+
</zeroOrMore>
|
28
|
+
</element>
|
29
|
+
RNG
|
30
|
+
end
|
31
|
+
|
32
|
+
it "correctly parses and rebuilds RNG" do
|
33
|
+
parsed = rng_parser.parse(rng_input)
|
34
|
+
rebuilt = builder.build(parsed, format: :rng)
|
35
|
+
expect(normalize_xml(rebuilt)).to eq(normalize_xml(rng_input))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "RNC parsing and building" do
|
40
|
+
let(:rnc_input) do
|
41
|
+
<<~RNC
|
42
|
+
element addressBook {
|
43
|
+
element card {
|
44
|
+
element name { text },
|
45
|
+
element email { text },
|
46
|
+
element note { text }?
|
47
|
+
}*
|
48
|
+
}
|
49
|
+
RNC
|
50
|
+
end
|
51
|
+
|
52
|
+
it "correctly parses and rebuilds RNC" do
|
53
|
+
parsed = rnc_parser.parse(rnc_input)
|
54
|
+
rebuilt = builder.build(parsed, format: :rnc)
|
55
|
+
expect(rebuilt.gsub(/\s+/, "")).to eq(rnc_input.gsub(/\s+/, ""))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "RNG to RNC conversion" do
|
60
|
+
let(:rng_input) do
|
61
|
+
<<~RNG
|
62
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
63
|
+
<element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
|
64
|
+
<zeroOrMore>
|
65
|
+
<element name="card">
|
66
|
+
<element name="name">
|
67
|
+
<text/>
|
68
|
+
</element>
|
69
|
+
<element name="email">
|
70
|
+
<text/>
|
71
|
+
</element>
|
72
|
+
</element>
|
73
|
+
</zeroOrMore>
|
74
|
+
</element>
|
75
|
+
RNG
|
76
|
+
end
|
77
|
+
|
78
|
+
let(:expected_rnc) do
|
79
|
+
<<~RNC
|
80
|
+
element addressBook {
|
81
|
+
element card {
|
82
|
+
element name { text },
|
83
|
+
element email { text }
|
84
|
+
}*
|
85
|
+
}
|
86
|
+
RNC
|
87
|
+
end
|
88
|
+
|
89
|
+
it "correctly converts RNG to RNC" do
|
90
|
+
parsed = rng_parser.parse(rng_input)
|
91
|
+
rnc = builder.build(parsed, format: :rnc)
|
92
|
+
expect(rnc.gsub(/\s+/, "")).to eq(expected_rnc.gsub(/\s+/, ""))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "RNC to RNG conversion" do
|
97
|
+
let(:rnc_input) do
|
98
|
+
<<~RNC
|
99
|
+
element addressBook {
|
100
|
+
element card {
|
101
|
+
element name { text },
|
102
|
+
element email { text }
|
103
|
+
}*
|
104
|
+
}
|
105
|
+
RNC
|
106
|
+
end
|
107
|
+
|
108
|
+
let(:expected_rng) do
|
109
|
+
<<~RNG
|
110
|
+
<element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
|
111
|
+
<zeroOrMore>
|
112
|
+
<element name="card">
|
113
|
+
<element name="name">
|
114
|
+
<text/>
|
115
|
+
</element>
|
116
|
+
<element name="email">
|
117
|
+
<text/>
|
118
|
+
</element>
|
119
|
+
</element>
|
120
|
+
</zeroOrMore>
|
121
|
+
</element>
|
122
|
+
RNG
|
123
|
+
end
|
124
|
+
|
125
|
+
it "correctly converts RNC to RNG" do
|
126
|
+
parsed = rnc_parser.parse(rnc_input)
|
127
|
+
rng = builder.build(parsed, format: :rng)
|
128
|
+
expect(rng.gsub(/\s+/, "")).to eq(expected_rng.gsub(/\s+/, ""))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "Complex schema parsing and building" do
|
133
|
+
let(:complex_rng_input) do
|
134
|
+
<<~RNG
|
135
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
136
|
+
<start>
|
137
|
+
<ref name="addressBook"/>
|
138
|
+
</start>
|
139
|
+
|
140
|
+
<define name="addressBook">
|
141
|
+
<element name="addressBook">
|
142
|
+
<zeroOrMore>
|
143
|
+
<ref name="card"/>
|
144
|
+
</zeroOrMore>
|
145
|
+
</element>
|
146
|
+
</define>
|
147
|
+
|
148
|
+
<define name="card">
|
149
|
+
<element name="card">
|
150
|
+
<ref name="name"/>
|
151
|
+
<ref name="email"/>
|
152
|
+
<optional>
|
153
|
+
<ref name="note"/>
|
154
|
+
</optional>
|
155
|
+
</element>
|
156
|
+
</define>
|
157
|
+
|
158
|
+
<define name="name">
|
159
|
+
<element name="name">
|
160
|
+
<text/>
|
161
|
+
</element>
|
162
|
+
</define>
|
163
|
+
|
164
|
+
<define name="email">
|
165
|
+
<element name="email">
|
166
|
+
<text/>
|
167
|
+
</element>
|
168
|
+
</define>
|
169
|
+
|
170
|
+
<define name="note">
|
171
|
+
<element name="note">
|
172
|
+
<text/>
|
173
|
+
</element>
|
174
|
+
</define>
|
175
|
+
</grammar>
|
176
|
+
RNG
|
177
|
+
end
|
178
|
+
|
179
|
+
it "correctly parses and rebuilds complex RNG" do
|
180
|
+
parsed = rng_parser.parse(complex_rng_input)
|
181
|
+
rebuilt = builder.build(parsed, format: :rng)
|
182
|
+
expect(rebuilt.gsub(/\s+/, "")).to eq(complex_rng_input.gsub(/\s+/, ""))
|
183
|
+
end
|
184
|
+
|
185
|
+
it "correctly converts complex RNG to RNC" do
|
186
|
+
parsed = rng_parser.parse(complex_rng_input)
|
187
|
+
rnc = builder.build(parsed, format: :rnc)
|
188
|
+
reparsed = rnc_parser.parse(rnc)
|
189
|
+
rng_again = builder.build(reparsed, format: :rng)
|
190
|
+
expect(rng_again.gsub(/\s+/, "")).to eq(complex_rng_input.gsub(/\s+/, ""))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/spec/rng_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rng"
|
4
|
+
require "xml/c14n"
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
# Enable flags like --only-failures and --next-failure
|
8
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
9
|
+
|
10
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
11
|
+
config.disable_monkey_patching!
|
12
|
+
|
13
|
+
config.expect_with :rspec do |c|
|
14
|
+
c.syntax = :expect
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add helper method for XML comparison
|
18
|
+
config.include(Module.new do
|
19
|
+
def normalize_xml(xml)
|
20
|
+
Xml::C14n.format(xml)
|
21
|
+
end
|
22
|
+
end)
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rng
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ribose
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: lutaml-model
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: parslet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: nokogiri
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- open.source@ribose.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".github/workflows/rake.yml"
|
63
|
+
- ".github/workflows/release.yml"
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rspec"
|
66
|
+
- ".rubocop.yml"
|
67
|
+
- Gemfile
|
68
|
+
- Rakefile
|
69
|
+
- lib/rng.rb
|
70
|
+
- lib/rng/attribute.rb
|
71
|
+
- lib/rng/builder.rb
|
72
|
+
- lib/rng/define.rb
|
73
|
+
- lib/rng/element.rb
|
74
|
+
- lib/rng/rnc_parser.rb
|
75
|
+
- lib/rng/rng_parser.rb
|
76
|
+
- lib/rng/schema.rb
|
77
|
+
- lib/rng/start.rb
|
78
|
+
- lib/rng/version.rb
|
79
|
+
- rng.gemspec
|
80
|
+
- sig/rng.rbs
|
81
|
+
- spec/rng/rnc_parser_spec.rb
|
82
|
+
- spec/rng/rng_parser_spec.rb
|
83
|
+
- spec/rng/schema_spec.rb
|
84
|
+
- spec/rng_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
homepage: https://github.com/lutaml/rng
|
87
|
+
licenses:
|
88
|
+
- BSD-2-Clause
|
89
|
+
metadata:
|
90
|
+
homepage_uri: https://github.com/lutaml/rng
|
91
|
+
source_code_uri: https://github.com/lutaml/rng
|
92
|
+
bug_tracker_uri: https://github.com/lutaml/rng/issues
|
93
|
+
rubygems_mfa_required: 'true'
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 3.0.0
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.5.22
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Library to parse and build RELAX NG (RNG) and RELAX NG Compact Syntax (RNC)
|
113
|
+
schemas.
|
114
|
+
test_files: []
|