craftbook-nbt 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.
@@ -0,0 +1,121 @@
1
+
2
+ require_relative 'lexer'
3
+
4
+ module CraftBook
5
+
6
+ module NBT
7
+
8
+ ##
9
+ # Parses a stringified NBT string and creates a {CompoundTag} from it.
10
+ #
11
+ # @param string_nbt [String] The stringified NBT code to parse.
12
+ #
13
+ # @raise [SyntaxError] When the source `string_nbt` is not valid S-NBT.
14
+ # @raise [ParseError] When a an incorrect value is specified for the type of tag it represents.
15
+ # @raise [ArgumentError] When `string_nbt` is `nil`
16
+ #
17
+ # @return [CompoundTag] The parsed {CompoundTag} instance.
18
+ #
19
+ # @note This method is not safe to call in parallel from multiple threads.
20
+ # @see https://minecraft.fandom.com/wiki/NBT_format#SNBT_format
21
+ def self.parse_snbt(string_nbt)
22
+ raise(ArgumentError, "input string cannot be nil or empty") if string_nbt.nil? || string_nbt.empty?
23
+ @pos = 0
24
+ @depth = 0
25
+ lexer = Tokenizer.new
26
+ @tokens = lexer.tokenize(string_nbt)
27
+ parse_object(@tokens.first)
28
+ end
29
+
30
+ private
31
+
32
+ def self.assert_type(expected, actual)
33
+ raise(SyntaxError, "expected #{expected} token, got #{actual}") unless expected == actual
34
+ end
35
+
36
+ def self.parse_name(token)
37
+ assert_type(:IDENTIFIER, token.type)
38
+ assert_type(:SEPARATOR, move_next.type)
39
+ # move_next
40
+ token.value
41
+ end
42
+
43
+ def self.parse_array(name, klass)
44
+ values = []
45
+ loop do
46
+ token = move_next
47
+ case token.type
48
+ when :END_ARRAY then break
49
+ when :COMMA then next
50
+ else values.push(token.value)
51
+ end
52
+ end
53
+
54
+ klass.new(name, *values)
55
+ end
56
+
57
+ def self.move_next
58
+ @pos += 1
59
+ token = @tokens[@pos] || raise(SyntaxError, 'unexpected end of input')
60
+ [:WHITESPACE, :COMMA].include?(token.type) ? move_next : token
61
+ end
62
+
63
+ def self.parse_list(name)
64
+ values = []
65
+ types = []
66
+ loop do
67
+ token = move_next
68
+ case token.type
69
+ when :COMMA then next
70
+ when :END_ARRAY then break
71
+ else
72
+ types.push(token.type)
73
+ values.push(parse_object(token))
74
+ end
75
+ end
76
+
77
+ return ListTag.new(name, Tag::TYPE_END) if types.empty?
78
+ raise(ParseError, "lists must contain only the same child type") unless types.uniq.size <= 1
79
+ ListTag.new(name, values.first.type, *values)
80
+ end
81
+
82
+ def self.parse_object(token)
83
+
84
+ name = nil
85
+ if token.type == :IDENTIFIER
86
+ name = parse_name(token)
87
+ token = move_next
88
+ end
89
+
90
+ case token.type
91
+ when :STRING then StringTag.new(name, token.value)
92
+ when :INT then IntTag.new(name, token.value)
93
+ when :DOUBLE then DoubleTag.new(name, token.value)
94
+ when :FLOAT then FloatTag.new(name, token.value)
95
+ when :BYTE then ByteTag.new(name, token.value)
96
+ when :SHORT then ShortTag.new(name, token.value)
97
+ when :LONG then LongTag.new(name, token.value)
98
+ when :BYTE_ARRAY then parse_array(name, ByteArrayTag)
99
+ when :INT_ARRAY then parse_array(name, IntArrayTag)
100
+ when :LONG_ARRAY then parse_array(name, LongArrayTag)
101
+ when :LIST_ARRAY then parse_list(name)
102
+ when :COMPOUND_BEGIN then parse_compound(token, name)
103
+ else raise(ParseError, "invalid token, expected object type, got :#{token.type}")
104
+ end
105
+ end
106
+
107
+ def self.parse_compound(token, name)
108
+ assert_type(:COMPOUND_BEGIN, token.type)
109
+ compound = CompoundTag.new(name)
110
+
111
+ loop do
112
+ token = move_next
113
+ break if token.type == :COMPOUND_END
114
+ next if token.type == :COMMA
115
+ compound.add(parse_object(token))
116
+ end
117
+
118
+ compound
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,74 @@
1
+ module CraftBook
2
+ module NBT
3
+ class Tokenizer
4
+
5
+ macro
6
+ nl \n|\r\n|\r|\f
7
+ w [\s]*
8
+ num -?([0-9]+|[0-9]*\.[0-9]+)
9
+ l_bracket \[
10
+ r_bracket \]
11
+ l_brace \{
12
+ r_brace \}
13
+
14
+ integer -?([0-9]+)
15
+ decimal -?[0-9]*\.[0-9]+
16
+ escape {unicode}|\\[^\n\r\f0-9A-Fa-f]
17
+ id [A-Za-z0-9-_]
18
+ rule
19
+ \{{w} { [:COMPOUND_BEGIN] }
20
+ {w}\} { [:COMPOUND_END] }
21
+
22
+ ".+?"(?=:) { [:IDENTIFIER, text.gsub!(/\A"|"\Z/, '') ] }
23
+ '.+?'(?=:) { [:IDENTIFIER, text.gsub!(/\A'|'\Z/, '') ] }
24
+ [A-Za-z0-9_-]+?(?=:) { [:IDENTIFIER, text] }
25
+ ".*?" { [:STRING, text.gsub!(/\A"|"\Z/, '') ] }
26
+ '.*?' { [:STRING, text.gsub!(/\A'|'\Z/, '') ] }
27
+
28
+ # Control Characters
29
+ {w}:{w} { [:SEPARATOR, text] }
30
+ {w},{w} { [:COMMA, text] }
31
+
32
+ # Collection Types
33
+
34
+ {l_bracket}B;{w}? { [:BYTE_ARRAY, text] }
35
+ {l_bracket}I;{w}? { [:INT_ARRAY, text] }
36
+ {l_bracket}L;{w}? { [:LONG_ARRAY, text] }
37
+ \[{w}? { [:LIST_ARRAY, text] }
38
+ {w}\] { [:END_ARRAY, text] }
39
+
40
+ # Numeric Types
41
+ {decimal}[Ff] { [:FLOAT, text.chop.to_f ] }
42
+ {decimal}[Dd]? { [:DOUBLE, text.tr('Dd', '').to_f ] }
43
+ {integer}[Bb] { [:BYTE, text.chop.to_i ] }
44
+ {integer}[Ss] { [:SHORT, text.chop.to_i ] }
45
+ {integer}[Ll] { [:LONG, text.chop.to_i ] }
46
+ {integer} { [:INT, text.to_i ] }
47
+
48
+ [\s]+ { [:WHITESPACE, text] }
49
+ [\S]+ { [:STRING, text] }
50
+ . { [:CHAR, text] }
51
+
52
+ inner
53
+
54
+ Token = Struct.new(:type, :value)
55
+
56
+ def tokenize(code)
57
+ scan_setup(code)
58
+ if block_given?
59
+ while token = next_token
60
+ yield Token.new(*token)
61
+ end
62
+ return self
63
+ end
64
+ tokens = []
65
+ while token = next_token
66
+ tokens << Token.new(*token)
67
+ end
68
+ tokens
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'snbt/lexer'
2
+ require_relative 'snbt/snbt'
@@ -0,0 +1,40 @@
1
+
2
+ module CraftBook
3
+
4
+ module NBT
5
+
6
+ ##
7
+ # Represents a UTF-8 encoded string.
8
+ class StringTag < ValueTag
9
+
10
+ ##
11
+ # @!attribute [rw] value
12
+ # @return [String] the value of the tag.
13
+
14
+ ##
15
+ # Creates a new instance of the {StringTag} class.
16
+ #
17
+ # @param name [String,NilClass] The name of the tag, or `nil` when unnamed.
18
+ # @param value [String,NilClass] The value of the tag.
19
+ def initialize(name, value = '')
20
+ super(TYPE_STRING, name, value)
21
+ end
22
+
23
+ def value=(value)
24
+ @value = String(value)
25
+ end
26
+
27
+ ##
28
+ # @return [String] the NBT tag as a formatted and human-readable string.
29
+ def to_s
30
+ "TAG_String(#{@name ? "\"#{@name}\"" : 'None'}): \"#{@value}\""
31
+ end
32
+
33
+ ##
34
+ # @return [String] the NBT tag as an SNBT string.
35
+ def stringify
36
+ "#{snbt_prefix}\"#{@value}\""
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,197 @@
1
+
2
+ module CraftBook
3
+ module NBT
4
+
5
+ ##
6
+ # @abstract
7
+ # Abstract base class for all tag types.
8
+ class Tag
9
+
10
+ ##
11
+ # Not a concrete tag, implies the end of a Compound tag during serialization.
12
+ TYPE_END = 0x00
13
+
14
+ ##
15
+ # A signed 8-bit integer in the range of `-128` to `127` inclusive.
16
+ TYPE_BYTE = 0x01
17
+
18
+ ##
19
+ # A signed 16-bit integer in the range of `-32768` to `32767` inclusive.
20
+ TYPE_SHORT = 0x02
21
+
22
+ ##
23
+ # A signed 32-bit integer in the range of `-2147483648` to `2147483647` inclusive.
24
+ TYPE_INT = 0x03
25
+
26
+ ##
27
+ # A signed 64-bit integer in the range of `-9223372036854775808` and `9223372036854775807` inclusive.
28
+ TYPE_LONG = 0x04
29
+
30
+ ##
31
+ # An IEEE-754 single-precision floating point number (NaN possible).
32
+ TYPE_FLOAT = 0x05
33
+
34
+ ##
35
+ # An IEEE-754 double-precision floating point number (NaN possible).
36
+ TYPE_DOUBLE = 0x06
37
+
38
+ ##
39
+ # A contiguous collection of signed 8-bit integers in the range of `-128` to `127` inclusive.
40
+ TYPE_BYTE_ARRAY = 0x07
41
+
42
+ ##
43
+ # A UTF-8 encoded string.
44
+ TYPE_STRING = 0x08
45
+
46
+ ##
47
+ # A collection of **unnamed** tags of the same type.
48
+ TYPE_LIST = 0x09
49
+
50
+ ##
51
+ # A collection of **named** tags, order not guaranteed.
52
+ TYPE_COMPOUND = 0x0A
53
+
54
+ ##
55
+ # A contiguous collection of signed 32-bit integers in the range of `-2147483648` to `2147483647` inclusive.
56
+ TYPE_INT_ARRAY = 0x0B
57
+
58
+ ##
59
+ # A contiguous collection of signed 64-bit integers in the range of `-9223372036854775808`
60
+ # and `9223372036854775807` inclusive.
61
+ TYPE_LONG_ARRAY = 0x0C
62
+
63
+ ##
64
+ # @return [Integer] one of the `TYPE_*` constants to describe the primitive NBT type.
65
+ attr_reader :type
66
+
67
+ ##
68
+ # @return [String?] the name of the tag, or `nil` if unnamed.
69
+ attr_reader :name
70
+
71
+ ##
72
+ # Creates a new instance of the {Tag} class.
73
+ # @param type [Integer] One of the `TAG_*` constants indicating the primitive tag type.
74
+ # @param name [String,NilClass] The name of the tag, or `nil` when unnamed.
75
+ def initialize(type, name)
76
+ @type = type || raise(TypeError, 'type cannot be nil')
77
+ @name = name
78
+ end
79
+
80
+ ##
81
+ # Sets the name of the tag.
82
+ # @param value [String] The value to set the tag name as.
83
+ # @return [String?] The name of the tag.
84
+ def name=(value)
85
+ @name = value.nil? ? nil : String(value)
86
+ end
87
+
88
+ ##
89
+ # @abstract
90
+ # @return [Hash{Symbol => Object}] the hash-representation of this object.
91
+ def to_h
92
+ { name: @name, type: @type }
93
+ end
94
+
95
+ ##
96
+ # Retrieves the NBT tag in JavaScript Object Notation (JSON) format.
97
+ #
98
+ # @param pretty [Boolean] Flag indicating if output should be formatted in a more human-readable structure.
99
+ # @param opts [{Symbol=>String}] Options for how the output is formatted when using `pretty` flag.
100
+ # @option opts [String] indent: (' ') The string used for indenting.
101
+ # @option opts [String] space: (' ') The string used for spaces.
102
+ # @option opts [String] array_nl: ("\n") The string used for newlines between array elements.
103
+ # @option opts [String] object_nl: ("\n") The string used for newlines between objects.
104
+ #
105
+ # @return [String] the JSON representation of this object.
106
+ def to_json(pretty = false, **opts)
107
+ pretty ? JSON.pretty_generate(to_h.compact, **opts) : to_h.compact.to_json
108
+ end
109
+
110
+ ##
111
+ # Parses a {Tag} object from a JSON string.
112
+ #
113
+ # @param json [String] A string in JSON format.
114
+ # @return [Tag] The deserialized {Tag} instance.
115
+ def self.parse(json)
116
+ hash = JSON.parse(json, symbolize_names: true )
117
+ raise(ParseError, 'invalid format, expected object') unless hash.is_a?(Hash)
118
+ from_hash(hash)
119
+ end
120
+
121
+ ##
122
+ # @abstract
123
+ # @raise [NotImplementedError] Method must be overridden in derived classes.
124
+ # @return [String] the NBT tag as an SNBT string.
125
+ def stringify
126
+ raise(NotImplementedError, "#{__method__} must be implemented in derived classes")
127
+ end
128
+
129
+ alias_method :snbt, :stringify
130
+ alias_method :to_hash, :to_h
131
+ alias_method :to_str, :to_s
132
+
133
+ ##
134
+ # Retrieves the NBT tag as a formatted and tree-structured string.
135
+ #
136
+ # @param indent [String] The string inserted for each level of indent.
137
+ #
138
+ # @see pretty_print
139
+ # @return [String] The NBT string as a formatted string.
140
+ def pretty(indent = ' ')
141
+ io = StringIO.new
142
+ pretty_print(io, 0, indent)
143
+ io.string
144
+ end
145
+
146
+ ##
147
+ # Outputs the NBT tag as a formatted and tree-structured string.
148
+ #
149
+ # @param io [IO,#puts] An IO-like object that responds to #puts.
150
+ # @param level [Integer] The indentation level.
151
+ # @param indent [String] The string inserted for each level of indent.
152
+ #
153
+ # @see pretty
154
+ # @return [void]
155
+ def pretty_print(io = STDOUT, level = 0, indent = ' ')
156
+ io.puts(indent * level + self.to_s)
157
+ end
158
+
159
+ protected
160
+
161
+ def snbt_prefix
162
+ @name ? "#{@name}:" : ''
163
+ end
164
+
165
+ def self.class_from_type(type)
166
+ case type
167
+ when Tag::TYPE_BYTE then ByteTag
168
+ when Tag::TYPE_SHORT then ShortTag
169
+ when Tag::TYPE_INT then IntTag
170
+ when Tag::TYPE_LONG then LongTag
171
+ when Tag::TYPE_FLOAT then FloatTag
172
+ when Tag::TYPE_DOUBLE then DoubleTag
173
+ when Tag::TYPE_BYTE_ARRAY then ByteArrayTag
174
+ when Tag::TYPE_STRING then StringTag
175
+ when Tag::TYPE_LIST then ListTag
176
+ when Tag::TYPE_COMPOUND then CompoundTag
177
+ when Tag::TYPE_INT_ARRAY then IntArrayTag
178
+ when Tag::TYPE_LONG_ARRAY then LongArrayTag
179
+ else raise(ParseError, "invalid tag type")
180
+ end
181
+ end
182
+
183
+ def self.from_hash(hash)
184
+
185
+ name = hash[:name]
186
+ type = hash[:type]
187
+ raise(ParseError, "invalid type") unless !type.nil? & type.between?(TYPE_END, TYPE_LONG_ARRAY)
188
+
189
+ tag = class_from_type(type).allocate
190
+ tag.instance_variable_set(:@name, name)
191
+ tag.instance_variable_set(:@type, type)
192
+ tag.send(:parse_hash, hash)
193
+ tag
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,220 @@
1
+
2
+ module CraftBook
3
+ module NBT
4
+
5
+ ##
6
+ # Provides an intuitive and simplified way of building a complete NBT document from scratch, using only basic
7
+ # values without the need of creating intermediate {Tag} objects.
8
+ class TagBuilder
9
+
10
+ ##
11
+ # @return [CompoundTag] the implicit top-level {CompoundTag} that the {TagBuilder} is creating.
12
+ attr_reader :root
13
+
14
+ ##
15
+ # Creates a new instance of the {TagBuilder} class.
16
+ # @param name [String,NilClass] the name of the implicit top-level {CompoundTag} being created.
17
+ def initialize(name)
18
+ @root = CompoundTag.new(name)
19
+ @stack = []
20
+ end
21
+
22
+ ##
23
+ # Creates a new {TagBuilder} instance within a block, returning the completed {CompoundTag} when the block
24
+ # closes.
25
+ #
26
+ # @param name [String,NilClass] the name of the implicit top-level {CompoundTag} that the {TagBuilder} is creating.
27
+ # @return [CompoundTag] The resulting {CompoundTag} that was created.
28
+ # @raise [LocalJumpError] when called without a block.
29
+ def self.create(name)
30
+ raise(LocalJumpError, 'block required') unless block_given?
31
+ builder = new(name)
32
+ yield builder
33
+ builder.result
34
+ end
35
+
36
+ ##
37
+ # Creates a new {TagBuilder} instance from an existing {CompoundTag}.
38
+ # @param compound_tag [CompoundTag] An existing {CompoundTag} instance.
39
+ # @return [TagBuilder] A newly created {TagBuilder}.
40
+ # @raise [TypeError] when `compound_tag` is not a {CompoundTag}.
41
+ def self.from(compound_tag)
42
+ raise(TypeError, "#{compound_tag} is not a #{CompoundTag}") unless compound_tag.is_a?(CompoundTag)
43
+
44
+ builder = allocate
45
+ builder.instance_variable_set(:@root, compound_tag)
46
+ builder.instance_variable_set(:@stack, Array.new)
47
+ builder
48
+ end
49
+
50
+ ##
51
+ # Adds an existing {Tag} instance as a child to the current node.
52
+ # @param tag [Tag] The {Tag} object to add.
53
+ #
54
+ # @yieldparam builder [TagBuilder] Yields the {TagBuilder} instance to the block.
55
+ # @return [self]
56
+ # @raise [TypeError] when `tag` is not a {Tag} instance or `nil`.
57
+ def add(tag)
58
+ raise(TypeError, "tag cannot be nil") unless tag.is_a?(Tag)
59
+
60
+ root = @stack.empty? ? @root : @stack.last
61
+
62
+ if root.is_a?(CompoundTag) && tag.name.nil?
63
+ warn("direct children of Compound tags must be named")
64
+ elsif root.is_a?(ListTag) && tag.name
65
+ tag.name = nil
66
+ end
67
+ root.push(tag)
68
+ self
69
+ end
70
+
71
+ alias_method :<<, :add
72
+ alias_method :push, :add
73
+
74
+ ##
75
+ # Creates a {ByteTag} from the specified value, and adds it to the current node.
76
+ # @param value [Integer] The value of the tag.
77
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
78
+ # @return [self]
79
+ def byte(name, value)
80
+ add(ByteTag.new(name, Integer(value)))
81
+ end
82
+
83
+ ##
84
+ # Creates a {ShortTag} from the specified value, and adds it to the current node.
85
+ # @param value [Integer] The value of the tag.
86
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
87
+ # @return [self]
88
+ def short(name, value)
89
+ add(ShortTag.new(name, Integer(value)))
90
+ end
91
+
92
+ ##
93
+ # Creates a {IntTag} from the specified value, and adds it to the current node.
94
+ # @param value [Integer] The value of the tag.
95
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
96
+ # @return [self]
97
+ def int(name, value)
98
+ add(IntTag.new(name, Integer(value)))
99
+ end
100
+
101
+ ##
102
+ # Creates a {LongTag} from the specified value, and adds it to the current node.
103
+ # @param value [Integer] The value of the tag.
104
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
105
+ # @return [self]
106
+ def long(name, value)
107
+ add(LongTag.new(name, Integer(value)))
108
+ end
109
+
110
+ ##
111
+ # Creates a {FloatTag} from the specified value, and adds it to the current node.
112
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
113
+ # @param value [Float] The value of the tag.
114
+ # @return [self]
115
+ def float(name, value)
116
+ add(FloatTag.new(name, Float(value)))
117
+ end
118
+
119
+ ##
120
+ # Creates a {DoubleTag} from the specified value, and adds it to the current node.
121
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
122
+ # @param value [Float] The value of the tag.
123
+ # @return [self]
124
+ def double(name, value)
125
+ add(DoubleTag.new(name, Float(value)))
126
+ end
127
+
128
+ ##
129
+ # Creates a {StringTag} from the specified value, and adds it to the current node.
130
+ # @param value [String,Object] The value of the tag.
131
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
132
+ # @return [self]
133
+ def string(name, value)
134
+ add(StringTag.new(name, String(value)))
135
+ end
136
+
137
+ ##
138
+ # Creates a {ByteArrayTag} from the specified values, and adds it to the current node.
139
+ # @param values [Array<Integer>,Enumerable] The child values of the tag.
140
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
141
+ # @return [self]
142
+ def byte_array(name, *values)
143
+ add(ByteArrayTag.new(name, *values))
144
+ end
145
+
146
+ ##
147
+ # Creates a {IntArrayTag} from the specified values, and adds it to the current node.
148
+ # @param values [Array<Integer>,Enumerable] The child values of the tag.
149
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
150
+ # @return [self]
151
+ def int_array(name, *values)
152
+ add(IntArrayTag.new(name, *values))
153
+ end
154
+
155
+ ##
156
+ # Creates a {LongArrayTag} from the specified values, and adds it to the current node.
157
+ # @param values [Array<Integer>,Enumerable] The child values of the tag.
158
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
159
+ # @return [self]
160
+ def long_array(name, *values)
161
+ add(LongArrayTag.new(name, *values))
162
+ end
163
+
164
+ ##
165
+ # Creates a {ListTag} from the specified value, and adds it to the current node.
166
+ #
167
+ # @param child_type [Integer] One of the `Tag::TYPE_*` constants indicating the type of children in this tag.
168
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
169
+ # @param children [Array<Tag>,Enumerable] The child values of the tag.
170
+ #
171
+ # @overload list(child_type, name = nil, children =nil, &block)
172
+ # When called with a block, creates a new node that is pushed onto the stack. All tags created within the
173
+ # block will be added to this new scope. The node is closed when the block exits.
174
+ # @yield Yields nothing to the block.
175
+ #
176
+ # @overload list(child_type, name = nil, children =nil)
177
+ # When called without a block, all values to be included must be present in the `children` argument.
178
+ #
179
+ # @return [self]
180
+ def list(name, child_type, *children)
181
+ list = ListTag.new(name, child_type, *children)
182
+
183
+ if block_given?
184
+ @stack.push(list)
185
+ yield
186
+ @stack.pop
187
+ end
188
+
189
+ add(list)
190
+ end
191
+
192
+ ##
193
+ # Creates a {CompoundTag} from the specified value, and adds it to the current node.
194
+ #
195
+ # @param name [String,NilClass] The name of the tag, or `nil` when adding to a {ListTag} node.
196
+ # @param children [Array<Tag>,Enumerable] The child values of the tag.
197
+ #
198
+ # @overload compound(name = nil, children =nil, &block)
199
+ # When called with a block, creates a new node that is pushed onto the stack. All tags created within the
200
+ # block will be added to this new scope. The node is closed when the block exits.
201
+ # @yield Yields nothing to the block.
202
+ #
203
+ # @overload compound(name = nil, children =nil)
204
+ # When called without a block, all values to be included must be present in the `children` argument.
205
+ #
206
+ # @return [self]
207
+ def compound(name, *children)
208
+ compound = CompoundTag.new(name, *children)
209
+
210
+ if block_given?
211
+ @stack.push(compound)
212
+ yield self
213
+ @stack.pop
214
+ end
215
+
216
+ add(compound)
217
+ end
218
+ end
219
+ end
220
+ end