craftbook-nbt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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