y-rb 0.6.0-x86_64-linux-gnu

Sign up to get free protection for your applications and to get access to all the features.
data/lib/y/xml.rb ADDED
@@ -0,0 +1,1141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ # rubocop:disable Metrics/ClassLength
5
+
6
+ # A XMLElement
7
+ #
8
+ # Someone should not instantiate an element directly, but use
9
+ # {Y::Doc#get_xml_element} instead
10
+ #
11
+ # @example
12
+ # doc = Y::Doc.new
13
+ # xml_element = doc.get_xml_element("my xml")
14
+ #
15
+ # puts xml_element.to_s
16
+ class XMLElement
17
+ # @!attribute [r] document
18
+ #
19
+ # @return [Y::Doc] The document this array belongs to
20
+ attr_accessor :document
21
+
22
+ # Create a new XMLElement instance
23
+ #
24
+ # @param doc [Y::Doc]
25
+ def initialize(doc = nil)
26
+ @document = doc || Y::Doc.new
27
+
28
+ super()
29
+ end
30
+
31
+ # Retrieve node at index
32
+ #
33
+ # @param index [Integer]
34
+ # @return [Y::XMLElement, nil]
35
+ def [](index)
36
+ node = document.current_transaction { |tx| yxml_element_get(tx, index) }
37
+ node&.document = document
38
+ node
39
+ end
40
+
41
+ # Create a node at index
42
+ #
43
+ # @param index [Integer]
44
+ # @param name [String] Name of node, e.g. `<p />`
45
+ # @return [Y::XMLElement]
46
+ # rubocop:disable Lint/Void
47
+ def []=(index, name)
48
+ node = document.current_transaction do |tx|
49
+ yxml_element_insert_element(tx, index, name)
50
+ end
51
+ node.document = document
52
+ node
53
+ end
54
+ # rubocop:enable Lint/Void
55
+
56
+ # Returns first child in list or nil if no child exists
57
+ #
58
+ # @return [Hash]
59
+ def attrs
60
+ document.current_transaction { |tx| yxml_element_attributes(tx) }
61
+ end
62
+
63
+ alias attributes attrs
64
+
65
+ # Returns first child in list or nil if no child exists
66
+ #
67
+ # @return [Y::XMLElement]
68
+ def first_child
69
+ child = document.current_transaction { |tx| yxml_element_first_child(tx) }
70
+ child&.document = document
71
+ child
72
+ end
73
+
74
+ # Insert text into element at given index
75
+ #
76
+ # Optional input is pushed to the text if provided
77
+ #
78
+ # @param index [Integer]
79
+ # @param input [String, nil]
80
+ # @return [Y::XMLText]
81
+ def insert_text(index, input = "")
82
+ text = document.current_transaction do |tx|
83
+ yxml_element_insert_text(tx, index, input)
84
+ end
85
+ text.document = document
86
+ text
87
+ end
88
+
89
+ # Retrieve element or text adjacent (next) to this element
90
+ #
91
+ # @return [Y::XMLElement, Y::XMLText, nil]
92
+ def next_sibling
93
+ node = document.current_transaction { |tx| yxml_element_next_sibling(tx) }
94
+ node&.document = document
95
+ node
96
+ end
97
+
98
+ # Attach listener to get notified about changes to the element
99
+ #
100
+ # This supports either a `Proc` or a `Block`.
101
+ #
102
+ # @example Receive changes via Proc
103
+ # doc = Y::Doc.new
104
+ # xml_element = doc.get_xml_element("my xml element")
105
+ # xml_element.attach ->(changes) { … }
106
+ #
107
+ # @example Receive changes via Block
108
+ # doc = Y::Doc.new
109
+ # xml_element = doc.get_xml_element("my xml element")
110
+ # xml_element.attach { |changes| … }
111
+ #
112
+ # @param callback [Proc]
113
+ # @param block [Block]
114
+ # @return [Integer] The subscription ID
115
+ def attach(callback = nil, &block)
116
+ return yxml_element_observe(callback) unless callback.nil?
117
+
118
+ yxml_element_observe(block.to_proc) unless block.nil?
119
+ end
120
+
121
+ # Retrieve parent element
122
+ #
123
+ # @return [Y::XMLElement, nil]
124
+ def parent
125
+ node = yxml_element_parent
126
+ node.document = document
127
+ node
128
+ end
129
+
130
+ # Retrieve element or text adjacent (previous) to this element
131
+ #
132
+ # @return [Y::XMLElement, Y::XMLText, nil]
133
+ def prev_sibling
134
+ node = document.current_transaction { |tx| yxml_element_prev_sibling(tx) }
135
+ node&.document = document
136
+ node
137
+ end
138
+
139
+ # Creates a new child an inserts at the end of the children list
140
+ #
141
+ # @param name [String]
142
+ # @return [Y::XMLElement]
143
+ def <<(name)
144
+ xml_element = document.current_transaction do |tx|
145
+ yxml_element_push_element_back(tx, name)
146
+ end
147
+ xml_element.document = document
148
+ xml_element
149
+ end
150
+
151
+ alias push_child <<
152
+
153
+ # Insert new text at the end of this elements child list
154
+ #
155
+ # The optional str argument initializes the text node with its value
156
+ #
157
+ # @param str [String]
158
+ # @return [Y::XMLText]
159
+ def push_text(str = "")
160
+ text = document.current_transaction do |tx|
161
+ yxml_element_push_text_back(tx, str)
162
+ end
163
+ text.document = document
164
+ text
165
+ end
166
+
167
+ # Number of children
168
+ #
169
+ # @return [Integer]
170
+ def size
171
+ document.current_transaction { |tx| yxml_element_size(tx) }
172
+ end
173
+
174
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
175
+
176
+ # Removes one or more children from XML Element
177
+ #
178
+ # @example Removes a single element
179
+ # doc = Y::Doc.new
180
+ #
181
+ # xml_element = doc.get_xml_element("my xml")
182
+ # xml_element << "A"
183
+ # xml_element << "B"
184
+ # xml_element << "C"
185
+ #
186
+ # xml_element.slice!(1)
187
+ #
188
+ # xml_element.to_s # <UNDEFINED><A></A><C></C></UNDEFINED>
189
+ #
190
+ # @overload slice!(n)
191
+ # Removes nth node from child list
192
+ #
193
+ # @overload slice!(start, length)
194
+ # Removes a range of nodes
195
+ #
196
+ # @overload slice!(range)
197
+ # Removes a range of nodes
198
+ #
199
+ # @return [void]
200
+ def slice!(*args)
201
+ document.current_transaction do |tx| # rubocop:disable Metrics/BlockLength
202
+ if args.empty?
203
+ raise ArgumentError,
204
+ "Provide one of `index`, `range`, `start, length` as arguments"
205
+ end
206
+
207
+ if args.size == 1
208
+ arg = args.first
209
+
210
+ if arg.is_a?(Range)
211
+ if arg.exclude_end?
212
+ yxml_element_remove_range(tx, arg.first,
213
+ arg.last - arg.first)
214
+ end
215
+ unless arg.exclude_end?
216
+ yxml_element_remove_range(tx, arg.first,
217
+ arg.last + 1 - arg.first)
218
+ end
219
+ return nil
220
+ end
221
+
222
+ if arg.is_a?(Numeric)
223
+ yxml_element_remove_range(tx, arg.to_int, 1)
224
+ return nil
225
+ end
226
+ end
227
+
228
+ if args.size == 2
229
+ first, second = args
230
+
231
+ if first.is_a?(Numeric) && second.is_a?(Numeric)
232
+ yxml_element_remove_range(tx, first, second)
233
+ return nil
234
+ end
235
+ end
236
+
237
+ raise ArgumentError, "Please check your arguments, can't slice."
238
+ end
239
+ end
240
+
241
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
242
+
243
+ # Tag name
244
+ #
245
+ # @return [String]
246
+ def tag
247
+ yxml_element_tag
248
+ end
249
+
250
+ # String representation of this node and all its children
251
+ #
252
+ # @return [String]
253
+ def to_s
254
+ document.current_transaction { |tx| yxml_element_to_s(tx) }
255
+ end
256
+
257
+ # Detach a listener
258
+ #
259
+ # @param subscription_id [Integer]
260
+ # @return [void]
261
+ def detach(subscription_id)
262
+ yxml_element_unobserve(subscription_id)
263
+ end
264
+
265
+ # Creates a new node and puts it in front of the child list
266
+ #
267
+ # @param name [String]
268
+ # @return [Y::XMLElement]
269
+ def unshift_child(name)
270
+ xml_element = document.current_transaction do |tx|
271
+ yxml_element_push_element_front(tx, name)
272
+ end
273
+ xml_element.document = document
274
+ xml_element
275
+ end
276
+
277
+ # Insert new text at the front of this elements child list
278
+ #
279
+ # The optional str argument initializes the text node with its value
280
+ #
281
+ # @param str [String]
282
+ # @return [Y::XMLText]
283
+ def unshift_text(str = "")
284
+ text = document.current_transaction do |tx|
285
+ yxml_element_push_text_front(tx, str)
286
+ end
287
+ text.document = document
288
+ text
289
+ end
290
+
291
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
292
+
293
+ # make attributes just work on an element in the form of `attr_name` and
294
+ # `attr_name=`
295
+ #
296
+ # @example Set and get an attribute
297
+ # doc = Y::Doc.new
298
+ # xml_element = doc.get_xml_element("my xml")
299
+ # xml_element.attr_name = "Hello"
300
+ #
301
+ # puts xml_element.attr_name # "Hello"
302
+ #
303
+ # @!visibility private
304
+ def method_missing(method_name, *args, &block)
305
+ is_setter = method_name.to_s.end_with?("=")
306
+
307
+ setter = method_name
308
+ setter += "=" unless is_setter
309
+ getter = method_name
310
+ getter = getter.to_s.slice(0...-1)&.to_sym if is_setter
311
+
312
+ define_singleton_method(setter.to_sym) do |new_val|
313
+ document.current_transaction do |tx|
314
+ yxml_element_insert_attribute(tx,
315
+ method_name.to_s
316
+ .delete_suffix("=")
317
+ .delete_prefix("attr_"),
318
+ new_val)
319
+ end
320
+ end
321
+
322
+ define_singleton_method(getter) do
323
+ document.current_transaction do |tx|
324
+ yxml_element_get_attribute(tx,
325
+ method_name.to_s.delete_prefix("attr_"))
326
+ end
327
+ end
328
+
329
+ if is_setter
330
+ value = args[0]
331
+ send(setter, value)
332
+ end
333
+ rescue StandardError
334
+ super(method_name, *args, &block)
335
+ end
336
+
337
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
338
+
339
+ # Make sure we only respond to attributes
340
+ # @!visibility private
341
+ def respond_to_missing?(method_name, include_private = false)
342
+ method_name.to_s.start_with?("attr_") || super
343
+ end
344
+
345
+ # @!method yxml_element_attributes
346
+ #
347
+ # @return [Hash]
348
+
349
+ # @!method yxml_element_first_child(tx)
350
+ #
351
+ # @param tx [Y::Transaction]
352
+ # @return [Y::XMLElement, Y::XMLText]
353
+
354
+ # @!method yxml_element_get_attribute(tx, name)
355
+ #
356
+ # @param tx [Y::Transaction]
357
+ # @param name [String]
358
+ # @return [String, nil]
359
+
360
+ # @!method yxml_element_get(tx, index)
361
+ #
362
+ # @param tx [Y::Transaction]
363
+ # @param index [Integer]
364
+ # @return [Y::XMLElement, Y::XMLText, nil]
365
+
366
+ # @!method yxml_element_insert_attribute(tx, name, value)
367
+ #
368
+ # @param tx [Y::Transaction]
369
+ # @param name [String]
370
+ # @param value [String]
371
+ # @return [String, nil]
372
+
373
+ # @!method yxml_element_insert_element(tx, index, name)
374
+ # Insert XML element into this XML element
375
+ #
376
+ # @!visibility private
377
+ # @param tx [Y::Transaction]
378
+ # @param index [Integer]
379
+ # @param name [String]
380
+ # @return [Y::XMLElement]
381
+
382
+ # @!method yxml_element_insert_text(tx, index, text)
383
+ #
384
+ # @param tx [Y::Transaction]
385
+ # @param index [Integer]
386
+ # @param text [String]
387
+ # @return [Y::XMLText]
388
+
389
+ # @!method yxml_element_next_sibling(tx)
390
+ #
391
+ # @param tx [Y::Transaction]
392
+ # @return [Y::XMLElement, XMLText, nil]
393
+
394
+ # @!method yxml_element_observe(callback)
395
+ #
396
+ # @param callback [Proc]
397
+ # @return [Integer] The subscription ID
398
+
399
+ # @!method yxml_element_parent()
400
+ #
401
+ # @return [Y::XMLElement, nil]
402
+
403
+ # @!method yxml_element_prev_sibling(tx)
404
+ #
405
+ # @param tx [Y::Transaction]
406
+ # @return [Y::XMLElement, XMLText, nil]
407
+
408
+ # @!method yxml_element_push_element_back(tx, name)
409
+ #
410
+ # @param tx [Y::Transaction]
411
+ # @param name [String]
412
+ # @return [Y::XMLElement]
413
+
414
+ # @!method yxml_element_push_element_front(tx, name)
415
+ #
416
+ # @param tx [Y::Transaction]
417
+ # @param name [String]
418
+ # @return [Y::XMLElement]
419
+
420
+ # @!method yxml_element_push_text_back(tx, text)
421
+ #
422
+ # @param tx [Y::Transaction]
423
+ # @param text [string]
424
+ # @return [Y::XMLText]
425
+
426
+ # @!method yxml_element_push_text_front(tx, text)
427
+ #
428
+ # @param tx [Y::Transaction]
429
+ # @param text [string]
430
+ # @return [Y::XMLText]
431
+
432
+ # @!method yxml_element_remove_attribute(tx, name)
433
+ #
434
+ # @param tx [Y::Transaction]
435
+ # @param name [String] name
436
+ # @return [void]
437
+
438
+ # @!method yxml_element_remove_range(tx, index, length)
439
+ #
440
+ # @param tx [Y::Transaction]
441
+ # @param index [Integer]
442
+ # @param length [Integer]
443
+ #
444
+ # @return [void]
445
+
446
+ # @!method yxml_element_size(tx)
447
+ #
448
+ # @param tx [Y::Transaction]
449
+ # @return [Integer]
450
+
451
+ # @!method yxml_element_tag
452
+ #
453
+ # @return [String]
454
+
455
+ # @!method yxml_element_to_s(tx)
456
+ #
457
+ # @param tx [Y::Transaction]
458
+ # @return [String]
459
+
460
+ # @!method yxml_element_unobserve(subscription_id)
461
+ #
462
+ # @param subscription_id [Integer]
463
+ # @return [void]
464
+ end
465
+
466
+ # A XMLText
467
+ #
468
+ # Someone should not instantiate a text directly, but use
469
+ # {Y::Doc#get_xml_text}, {Y::XMLElement#insert_text},
470
+ # {Y::XMLElement#push_text}, {Y::XMLElement#unshift_text} instead.
471
+ #
472
+ # The XMLText API is similar to {Y::Text}, but adds a few methods to make it
473
+ # easier to work in structured XML documents.
474
+ #
475
+ # @example
476
+ # doc = Y::Doc.new
477
+ # xml_text = doc.get_xml_text("my xml text")
478
+ #
479
+ # puts xml_text.to_s
480
+ class XMLText
481
+ # @!attribute [r] document
482
+ #
483
+ # @return [Y::Doc] The document this array belongs to
484
+ attr_accessor :document
485
+
486
+ # Create a new XMLText instance
487
+ #
488
+ # @param doc [Y::Doc]
489
+ def initialize(doc = nil)
490
+ @document = doc || Y::Doc.new
491
+
492
+ super()
493
+ end
494
+
495
+ # Push a string to the end of the text node
496
+ #
497
+ # @param str [String]
498
+ # @return {void}
499
+ def <<(str)
500
+ document.current_transaction { |tx| yxml_text_push(tx, str) }
501
+ end
502
+
503
+ alias push <<
504
+
505
+ # Attach a listener to get notified about changes
506
+ #
507
+ # @param callback [Proc]
508
+ # @return [Integer] subscription_id
509
+ def attach(callback = nil, &block)
510
+ yxml_text_observe(callback) unless callback.nil?
511
+ yxml_text_observe(block.to_proc) unless block.nil?
512
+ end
513
+
514
+ # Return text attributes
515
+ #
516
+ # @return [Hash]
517
+ def attrs
518
+ document.current_transaction { |tx| yxml_text_attributes(tx) }
519
+ end
520
+
521
+ # Detach a listener
522
+ #
523
+ # @param subscription_id [Integer]
524
+ # @return [void]
525
+ def detach(subscription_id)
526
+ yxml_text_unobserve(subscription_id)
527
+ end
528
+
529
+ # Format text
530
+ #
531
+ # @param index [Integer]
532
+ # @param length [Integer]
533
+ # @param attrs [Hash]
534
+ # @return [void]
535
+ def format(index, length, attrs)
536
+ document.current_transaction do |tx|
537
+ yxml_text_format(tx, index, length, attrs)
538
+ end
539
+ end
540
+
541
+ # rubocop:disable Metrics/MethodLength
542
+
543
+ # Insert a value at position and with optional attributes. This method is
544
+ # similar to [String#insert](https://ruby-doc.org/core-3.1.2/String.html),
545
+ # except for the optional third `attrs` argument.
546
+ #
547
+ # @example Insert a string at position
548
+ # doc = Y::Doc.new
549
+ # text = doc.get_text("my text")
550
+ # text << "Hello, "
551
+ #
552
+ # text.insert(7, "World!")
553
+ #
554
+ # puts text.to_s == "Hello, World!" # true
555
+ #
556
+ # The value can be any of the supported types:
557
+ # - Boolean
558
+ # - String
559
+ # - Numeric
560
+ # - Array (where element types must be supported)
561
+ # - Hash (where the the types of key and values must be supported)
562
+ #
563
+ # @param index [Integer]
564
+ # @param value [String, Float, Integer, Array, Hash, Boolean]
565
+ # @param attrs [Hash, nil]
566
+ # @return [void]
567
+ def insert(index, value, attrs = nil)
568
+ document.current_transaction do |tx|
569
+ if value.is_a?(String)
570
+ yxml_text_insert(tx, index, value) if attrs.nil?
571
+ unless attrs.nil?
572
+ yxml_text_insert_with_attrs(tx, index, value,
573
+ attrs)
574
+ end
575
+
576
+ return nil
577
+ end
578
+
579
+ if can_insert?(value)
580
+ yxml_text_insert_embed(tx, index, value) if attrs.nil?
581
+ unless attrs.nil?
582
+ yxml_text_insert_embed_with_attrs(tx, index, value,
583
+ attrs)
584
+ end
585
+
586
+ return nil
587
+ end
588
+
589
+ raise ArgumentError,
590
+ "Can't insert value. `#{value.class.name}` isn't supported."
591
+ end
592
+ end
593
+
594
+ # rubocop:enable Metrics/MethodLength
595
+
596
+ # Return length of string
597
+ #
598
+ # @return [void]
599
+ def length
600
+ document.current_transaction { |tx| yxml_text_length(tx) }
601
+ end
602
+
603
+ alias size length
604
+
605
+ # Return adjacent XMLElement or XMLText node (next)
606
+ #
607
+ # @return [Y::XMLElement, Y::XMLText, nil]
608
+ def next_sibling
609
+ node = document.current_transaction { |tx| yxml_text_next_sibling(tx) }
610
+ node.document = document
611
+ node
612
+ end
613
+
614
+ # Return parent XMLElement
615
+ #
616
+ # @return [Y::XMLElement, nil]
617
+ def parent
618
+ node = yxml_text_parent
619
+ node.document = document
620
+ node
621
+ end
622
+
623
+ # Return adjacent XMLElement or XMLText node (prev)
624
+ #
625
+ # @return [Y::XMLElement, Y::XMLText, nil]
626
+ def prev_sibling
627
+ node = document.current_transaction { |tx| yxml_text_prev_sibling(tx) }
628
+ node&.document = document
629
+ node
630
+ end
631
+
632
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
633
+
634
+ # Removes a part from text
635
+ #
636
+ # **Attention:** In comparison to String#slice, {XMLText#slice!} will not
637
+ # return the substring that gets removed. Even this being technically
638
+ # possible, it requires us to read the substring before removing it, which
639
+ # is not desirable in most situations.
640
+ #
641
+ # @example Removes a single character
642
+ # doc = Y::Doc.new
643
+ #
644
+ # text = doc.get_xml_text("my xml text")
645
+ # text << "Hello"
646
+ #
647
+ # text.slice!(0)
648
+ #
649
+ # text.to_s == "ello" # true
650
+ #
651
+ # @example Removes a range of characters
652
+ # doc = Y::Doc.new
653
+ #
654
+ # text = doc.get_xml_text("my xml text")
655
+ # text << "Hello"
656
+ #
657
+ # text.slice!(1..2)
658
+ # text.to_s == "Hlo" # true
659
+ #
660
+ # text.slice!(1...2)
661
+ # text.to_s == "Ho" # true
662
+ #
663
+ # @example Removes a range of chars from start and for given length
664
+ # doc = Y::Doc.new
665
+ #
666
+ # text = doc.get_xml_text("my xml text")
667
+ # text << "Hello"
668
+ #
669
+ # text.slice!(0, 3)
670
+ #
671
+ # text.to_s == "lo" # true
672
+ #
673
+ # @overload slice!(index)
674
+ # Removes a single character at index
675
+ #
676
+ # @overload slice!(start, length)
677
+ # Removes a range of characters
678
+ #
679
+ # @overload slice!(range)
680
+ # Removes a range of characters
681
+ #
682
+ # @return [void]
683
+ def slice!(*args)
684
+ document.current_transaction do |tx|
685
+ if args.empty?
686
+ raise ArgumentError,
687
+ "Provide one of `index`, `range`, `start, length` as arguments"
688
+ end
689
+
690
+ if args.size == 1
691
+ arg = args.first
692
+
693
+ if arg.is_a?(Range)
694
+ yxml_text_remove_range(tx, arg.first, arg.last - arg.first)
695
+ return nil
696
+ end
697
+
698
+ if arg.is_a?(Numeric)
699
+ yxml_text_remove_range(tx, arg.to_int, 1)
700
+ return nil
701
+ end
702
+ end
703
+
704
+ if args.size == 2
705
+ first, second = args
706
+
707
+ if first.is_a?(Numeric) && second.is_a?(Numeric)
708
+ yxml_text_remove_range(tx, first, second)
709
+ return nil
710
+ end
711
+ end
712
+
713
+ raise ArgumentError, "Please check your arguments, can't slice."
714
+ end
715
+ end
716
+
717
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
718
+
719
+ # Returns string representation of XMLText
720
+ #
721
+ # @return [String]
722
+ def to_s
723
+ document.current_transaction { |tx| yxml_text_to_s(tx) }
724
+ end
725
+
726
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
727
+
728
+ # make attributes just work on an element in the form of `attr_name` and
729
+ # `attr_name=`
730
+ #
731
+ # @example Set and get an attribute
732
+ # doc = Y::Doc.new
733
+ # xml_element = doc.get_xml_element("my xml")
734
+ # xml_element.attr_name = "Hello"
735
+ #
736
+ # puts xml_element.attr_name # "Hello"
737
+ #
738
+ # @!visibility private
739
+ def method_missing(method_name, *args, &block)
740
+ is_setter = method_name.to_s.end_with?("=")
741
+
742
+ setter = method_name
743
+ setter += "=" unless is_setter
744
+ getter = method_name
745
+ getter = getter.to_s.slice(0...-1)&.to_sym if is_setter
746
+
747
+ define_singleton_method(setter.to_sym) do |new_val|
748
+ document.current_transaction do |tx|
749
+ yxml_text_insert_attribute(tx,
750
+ method_name.to_s
751
+ .delete_suffix("=")
752
+ .delete_prefix("attr_"),
753
+ new_val)
754
+ end
755
+ end
756
+
757
+ define_singleton_method(getter) do
758
+ document.current_transaction do |tx|
759
+ yxml_text_get_attribute(tx, method_name.to_s.delete_prefix("attr_"))
760
+ end
761
+ end
762
+
763
+ if is_setter
764
+ value = args[0]
765
+ send(setter, value)
766
+ end
767
+ rescue StandardError
768
+ super(method_name, *args, &block)
769
+ end
770
+
771
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
772
+
773
+ # Make sure we only respond to attributes
774
+ # @!visibility private
775
+ def respond_to_missing?(method_name, include_private = false)
776
+ method_name.to_s.start_with?("attr_") || super
777
+ end
778
+
779
+ private
780
+
781
+ def can_insert?(value)
782
+ value.is_a?(NilClass) ||
783
+ value.is_a?(Symbol) ||
784
+ [true, false].include?(value) ||
785
+ value.is_a?(Numeric) ||
786
+ value.is_a?(Enumerable) ||
787
+ value.is_a?(Hash)
788
+ end
789
+
790
+ # @!method yxml_text_attributes
791
+ #
792
+ # @return [Hash]
793
+
794
+ # @!method yxml_text_format(tx, index, length, attrs)
795
+ #
796
+ # @param tx [Y::Transaction]
797
+ # @param index [Integer]
798
+ # @param length [Integer]
799
+ # @param attrs [Hash]
800
+ # @return [void]
801
+
802
+ # @!method yxml_text_get_attribute(tx, name)
803
+ #
804
+ # @param tx [Y::Transaction]
805
+ # @param name [String]
806
+ # @return [String, nil]
807
+
808
+ # @!method yxml_text_insert(tx, index, str)
809
+ #
810
+ # @param tx [Y::Transaction]
811
+ # @param index [Integer]
812
+ # @param str [String]
813
+ # @return [void]
814
+
815
+ # @!method yxml_text_insert_attribute(tx, name, value)
816
+ #
817
+ # @param tx [Y::Transaction]
818
+ # @param name [String] name
819
+ # @param value [String] value
820
+ # @return [void]
821
+
822
+ # @!method yxml_text_insert_with_attrs(tx, index, value, attrs)
823
+ #
824
+ # @param tx [Y::Transaction]
825
+ # @param index [Integer]
826
+ # @param value [String]
827
+ # @param attrs [Hash]
828
+ # @return [void]
829
+
830
+ # @!method yxml_text_insert_embed(tx, index, value)
831
+ #
832
+ # @param tx [Y::Transaction]
833
+ # @param index [Integer]
834
+ # @param value [String]
835
+ # @return [void]
836
+
837
+ # @!method yxml_text_insert_embed_with_attrs(tx, index, value, attrs)
838
+ #
839
+ # @param tx [Y::Transaction]
840
+ # @param index [Integer]
841
+ # @param value [true, false, Float, Integer, Array, Hash]
842
+ # @param attrs [Hash]
843
+ # @return [void]
844
+
845
+ # @!method yxml_text_length(tx)
846
+ #
847
+ # @param tx [Y::Transaction]
848
+ # @return [Integer]
849
+
850
+ # @!method yxml_text_next_sibling(tx)
851
+ #
852
+ # @param tx [Y::Transaction]
853
+ # @return [Y::XMLElement, Y::XMLText, nil]
854
+
855
+ # @!method yxml_text_observe(callback)
856
+ #
857
+ # @param callback [Proc]
858
+ # @return [Integer] A subscription ID
859
+
860
+ # @!method yxml_text_parent
861
+ #
862
+ # @return [Y::XMLElement, nil]
863
+
864
+ # @!method yxml_text_prev_sibling(tx)
865
+ #
866
+ # @param tx [Y::Transaction]
867
+ # @return [Y::XMLElement, Y::XMLText, nil]
868
+
869
+ # @!method yxml_text_push(tx, str)
870
+ #
871
+ # @param tx [Y::Transaction]
872
+ # @param str [String]
873
+ # @return [void]
874
+
875
+ # @!method yxml_text_remove_range(tx, index, length)
876
+ #
877
+ # @param tx [Y::Transaction]
878
+ # @param index [Integer]
879
+ # @param length [Integer]
880
+ # @return [void]
881
+
882
+ # @!method yxml_text_to_s(tx)
883
+ #
884
+ # @param tx [Y::Transaction]
885
+ # @return [void]
886
+
887
+ # @!method yxml_text_unobserve(subscription_id)
888
+ #
889
+ # @param subscription_id [Integer]
890
+ # @return [void]
891
+ end
892
+
893
+ # @!visibility private
894
+ class XMLFragment
895
+ # @!attribute [r] document
896
+ #
897
+ # @return [Y::Doc] The document this array belongs to
898
+ attr_accessor :document
899
+
900
+ # Create a new XMLElement instance
901
+ #
902
+ # @param doc [Y::Doc]
903
+ def initialize(doc = nil)
904
+ @document = doc || Y::Doc.new
905
+
906
+ super()
907
+ end
908
+
909
+ # Retrieve node at index
910
+ #
911
+ # @param index [Integer]
912
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
913
+ def [](index)
914
+ node = document.current_transaction { |tx| yxml_fragment_get(tx, index) }
915
+ node&.document = document
916
+ node
917
+ end
918
+
919
+ # Create a node at index
920
+ #
921
+ # @param index [Integer]
922
+ # @param name [String] Name of node, e.g. `<p />`
923
+ # @return [Y::XMLElement]
924
+ # rubocop:disable Lint/Void
925
+ def []=(index, name)
926
+ node = document.current_transaction do |tx|
927
+ yxml_fragment_insert(tx, index, name)
928
+ end
929
+ node.document = document
930
+ node
931
+ end
932
+ # rubocop:enable Lint/Void
933
+
934
+ # Retrieve first child
935
+ #
936
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
937
+ def first_child
938
+ node = yxml_fragment_first_child
939
+ node&.document = document
940
+ node
941
+ end
942
+
943
+ # Retrieve child at index
944
+ #
945
+ # @param [Integer] index
946
+ # @param [String] tag
947
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
948
+ def insert(index, tag)
949
+ document.current_transaction { |tx| yxml_fragment_insert(tx, index, tag) }
950
+ end
951
+
952
+ # Length of the fragment
953
+ #
954
+ # @return [Integer]
955
+ def length
956
+ document.current_transaction { |tx| yxml_fragment_len(tx) }
957
+ end
958
+
959
+ alias size length
960
+
961
+ # Retrieve parent element
962
+ #
963
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
964
+ def parent
965
+ node = yxml_fragment_parent
966
+ node.document = document
967
+ node
968
+ end
969
+
970
+ # Creates a new child an inserts at the end of the children list
971
+ #
972
+ # @param name [String]
973
+ # @return [Y::XMLElement]
974
+ def <<(name)
975
+ xml_element = document.current_transaction do |tx|
976
+ yxml_fragment_push_back(tx, name)
977
+ end
978
+ xml_element.document = document
979
+ xml_element
980
+ end
981
+
982
+ alias push <<
983
+
984
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
985
+
986
+ # Removes one or more children from XML Fragment
987
+ #
988
+ # @example Removes a single element
989
+ # doc = Y::Doc.new
990
+ #
991
+ # xml_fragment = doc.get_xml_fragment("my xml fragment")
992
+ # xml_fragment << "A"
993
+ # xml_fragment << "B"
994
+ # xml_fragment << "C"
995
+ #
996
+ # xml_fragment.slice!(1)
997
+ #
998
+ # xml_fragment.to_s # <UNDEFINED><A></A><C></C></UNDEFINED>
999
+ #
1000
+ # @overload slice!(n)
1001
+ # Removes nth node from child list
1002
+ #
1003
+ # @overload slice!(start, length)
1004
+ # Removes a range of nodes
1005
+ #
1006
+ # @overload slice!(range)
1007
+ # Removes a range of nodes
1008
+ #
1009
+ # @return [void]
1010
+ def slice!(*args)
1011
+ document.current_transaction do |tx| # rubocop:disable Metrics/BlockLength
1012
+ if args.empty?
1013
+ raise ArgumentError,
1014
+ "Provide one of `index`, `range`, `start, length` as arguments"
1015
+ end
1016
+
1017
+ if args.size == 1
1018
+ arg = args.first
1019
+
1020
+ if arg.is_a?(Range)
1021
+ if arg.exclude_end?
1022
+ yxml_fragment_remove_range(tx, arg.first,
1023
+ arg.last - arg.first)
1024
+ end
1025
+ unless arg.exclude_end?
1026
+ yxml_fragment_remove_range(tx, arg.first,
1027
+ arg.last + 1 - arg.first)
1028
+ end
1029
+ return nil
1030
+ end
1031
+
1032
+ if arg.is_a?(Numeric)
1033
+ yxml_fragment_remove_range(tx, arg.to_int, 1)
1034
+ return nil
1035
+ end
1036
+ end
1037
+
1038
+ if args.size == 2
1039
+ first, second = args
1040
+
1041
+ if first.is_a?(Numeric) && second.is_a?(Numeric)
1042
+ yxml_fragment_remove_range(tx, first, second)
1043
+ return nil
1044
+ end
1045
+ end
1046
+
1047
+ raise ArgumentError, "Please check your arguments, can't slice."
1048
+ end
1049
+ end
1050
+
1051
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
1052
+
1053
+ # Traverse over the successors of the current XML element and return
1054
+ # a flat representation of all successors.
1055
+ #
1056
+ # @return [Array<Y::XMLElement, Y::XMLFragment, Y::XMLText]
1057
+ def successors
1058
+ document.current_transaction { |tx| yxml_fragment_successors(tx) }
1059
+ end
1060
+
1061
+ # Returns string representation of XMLFragment
1062
+ #
1063
+ # @return [String]
1064
+ def to_s
1065
+ document.current_transaction { |tx| yxml_fragment_to_s(tx) }
1066
+ end
1067
+
1068
+ # Creates a new node and puts it in front of the child list
1069
+ #
1070
+ # @param name [String]
1071
+ # @return [Y::XMLElement]
1072
+ def unshift(name)
1073
+ xml_element = document.current_transaction do |tx|
1074
+ yxml_fragment_push_front(tx, name)
1075
+ end
1076
+ xml_element.document = document
1077
+ xml_element
1078
+ end
1079
+
1080
+ # @!method yxml_fragment_first_child
1081
+ #
1082
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
1083
+
1084
+ # @!method yxml_fragment_get(tx, index)
1085
+ #
1086
+ # @param [Y::Transaction] tx
1087
+ # @param [Integer] index
1088
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
1089
+
1090
+ # @!method yxml_fragment_insert(tx, index, tag)
1091
+ #
1092
+ # @param tx [Y::Transaction]
1093
+ # @param [Integer] index
1094
+ # @param [String] tag
1095
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
1096
+
1097
+ # @!method yxml_fragment_len(tx)
1098
+ #
1099
+ # @param tx [Y::Transaction]
1100
+ # @return [Integer]
1101
+
1102
+ # @!method yxml_fragment_parent
1103
+ #
1104
+ # @return [Y::XMLElement, Y::XMLFragment, Y::XMLText, nil]
1105
+
1106
+ # @!method yxml_fragment_push_back(tx, tag)
1107
+ #
1108
+ # @param tx [Y::Transaction]
1109
+ # @param [String] tag
1110
+ # @return [Y::XMLElement]
1111
+
1112
+ # @!method yxml_fragment_push_front(tx, tag)
1113
+ #
1114
+ # @param tx [Y::Transaction]
1115
+ # @param [String] tag
1116
+ # @return [Y::XMLElement]
1117
+
1118
+ # @!method yxml_fragment_remove(tx, index)
1119
+ #
1120
+ # @param tx [Y::Transaction]
1121
+ # @param [Integer] index
1122
+
1123
+ # @!method yxml_fragment_remove_range(tx, index, length)
1124
+ #
1125
+ # @param tx [Y::Transaction]
1126
+ # @param [Integer] index
1127
+ # @param [Integer] length
1128
+
1129
+ # @!method yxml_fragment_successors(tx)
1130
+ #
1131
+ # @param tx [Y::Transaction]
1132
+ # @return [Array<Y::XMLElement, Y::XMLFragment, Y::XMLText>]
1133
+
1134
+ # @!method yxml_fragment_to_s(tx)
1135
+ #
1136
+ # @param tx [Y::Transaction]
1137
+ # @return [String]
1138
+ end
1139
+
1140
+ # rubocop:enable Metrics/ClassLength
1141
+ end