nokogiri-xml-range 0.1.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2b9a05b01ac98bf6a41a88fa74ee6e7c12caebdf
4
+ data.tar.gz: 491dc04b1eb68735c18330a472b3645f62c01ac7
5
+ SHA512:
6
+ metadata.gz: d197d502e3fc10c82510bc074ecdd04a728dffff385eb97f1e304504d7f325f92340285c66ab1abe99492f5fd222477bb16c2796970f492a6386ce1bc8374a74
7
+ data.tar.gz: 1f2606fa9df9778aa7c2ef4289d24f1469a8242e5ccbbda4cb2bda073ff581320442945d74c17ee1843fa808c804af53b1ba53f974fec653095424ee22eda144
@@ -0,0 +1 @@
1
+ service_name: travis-ci
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
@@ -0,0 +1,2 @@
1
+ doc/
2
+ pkg/
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - ruby-head
4
+ - 2.2.3
5
+ - 2.1.7
@@ -0,0 +1 @@
1
+ --markup markdown --title "Nokogiri::XML::Range Documentation" --protected
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
@@ -0,0 +1,4 @@
1
+ ### 0.1.0 / 2015-09-05
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,155 @@
1
+ Nokogiri::XML::Range
2
+ ====================
3
+
4
+ [![Build Status](https://travis-ci.org/KitaitiMakoto/nokogiri-xml-range.svg?branch=master)](https://travis-ci.org/KitaitiMakoto/nokogiri-xml-range)
5
+ [![Coverage Status](https://coveralls.io/repos/KitaitiMakoto/nokogiri-xml-range/badge.svg?branch=master&service=github)](https://coveralls.io/github/KitaitiMakoto/nokogiri-xml-range?branch=master)
6
+
7
+ * [Homepage](https://rubygems.org/gems/nokogiri-xml-range)
8
+ * [Documentation](http://rubydoc.info/gems/nokogiri-xml-range)
9
+ * [Email](mailto:KitaitiMakoto at gmail.com)
10
+
11
+ DOM Range implementation on Nokogiri
12
+
13
+ Description
14
+ -----------
15
+
16
+ [Nokogiri][] DOM Range Implementatin based on [DOM Standard specification][range spec].
17
+
18
+ [Nokogiri]: http://www.nokogiri.org/
19
+ [range spec]: https://dom.spec.whatwg.org/#ranges
20
+
21
+ Features
22
+ --------
23
+
24
+ `Nokogiri::XML::Range` expresses a range on DOM tree. It can:
25
+
26
+ * test a node is in the range or not,
27
+ * delete contents the range expresses from DOM tree,
28
+ * extract contents alike,
29
+ * clone contents alike,
30
+ * surround the range by specified DOM element,
31
+ * and so on...
32
+
33
+ Examples
34
+ --------
35
+
36
+ ### Initialization ###
37
+
38
+ require 'nokogiri/xml/range'
39
+
40
+ doc = Nokogiri.XML(<<EOX)
41
+ <root>
42
+ <parent>
43
+ <child>child 1</child>
44
+ <child>child 2</child>
45
+ </parent>
46
+ </root>
47
+ EOX
48
+ parent = doc.search('parent')[0]
49
+ child1 = doc.search('child')[0]
50
+ child2 = doc.search('child')[1]
51
+ child1_text = child1.child
52
+ child2_text = child2.child
53
+ # Initialize range with nodes and offsets of start and end point
54
+ range = Nokogiri::XML::Range.new(child1_text, 0, child2_text, 5)
55
+ # This range expresses `child 1</child>\n <child>child`
56
+
57
+ ### Deleting contents ###
58
+
59
+ range.delete_contents
60
+ puts doc
61
+ # <?xml version="1.0"?>
62
+ # <root>
63
+ # <parent>
64
+ # <child></child><child> 2</child>
65
+ # </parent>
66
+ # </root>
67
+
68
+ ### Extracting contents ###
69
+
70
+ `Nokogiri::XML::Range#extract_contents` remove the range from DOM tree and returns contents in the range.
71
+
72
+ extracted = range.extract_contents
73
+ # => #(DocumentFragment:0x3fa87891eaa0 {
74
+ # name = "#document-fragment",
75
+ # children = [
76
+ # #(Element:0x3fa87891e12c { name = "child", children = [ #(Text "child 1")] }),
77
+ # #(Text "\n "),
78
+ # #(Element:0x3fa8789177f0 { name = "child", children = [ #(Text "child")] })]
79
+ # })
80
+ puts doc
81
+ # <?xml version="1.0"?>
82
+ # <root>
83
+ # <parent>
84
+ # <child></child><child> 2</child>
85
+ # </parent>
86
+ # </root>
87
+ puts extracted
88
+ # <child>child 1</child>
89
+ # <child>child</child>
90
+
91
+ ### Cloning contents ###
92
+
93
+ cloned = range.clone_contents
94
+ # => #(DocumentFragment:0x3fa87809fb90 {
95
+ # name = "#document-fragment",
96
+ # children = [
97
+ # #(Element:0x3fa87809e394 { name = "child", children = [ #(Text "child 1")] }),
98
+ # #(Text "\n "),
99
+ # #(Element:0x3fa87808dcd8 { name = "child", children = [ #(Text "child")] })]
100
+ # })
101
+ puts cloned
102
+ # <child>child 1</child>
103
+ # <child>child</child>
104
+
105
+ `Nokogiri::XML::Range#clone_contents` doesn't affect original DOM tree.
106
+
107
+ ### Inserting node ###
108
+
109
+ `Nokogiri::XML::Range#insert_node` inserts a node just before the range start point.
110
+
111
+ text = Nokogiri::XML::Text.new('inserted', doc)
112
+ # => #(Text "inserted")
113
+ range.insert_node text
114
+ puts doc
115
+ # <?xml version="1.0"?>
116
+ # <root>
117
+ # <parent>
118
+ # <child>insertedchild 1</child>
119
+ # <child>child 2</child>
120
+ # </parent>
121
+ # </root>
122
+
123
+ ### Surrounding range contents ###
124
+
125
+ range = Nokogiri::XML::Range.new(child1_text, 6, child1_text, 7)
126
+ number = Nokogiri::XML::Element.new('number', doc)
127
+ range.surround_contents number
128
+ puts doc
129
+ # <?xml version="1.0"?>
130
+ # <root>
131
+ # <parent>
132
+ # <child>child <number>1</number></child>
133
+ # <child>child 2</child>
134
+ # </parent>
135
+ # </root>
136
+
137
+ Requirements
138
+ ------------
139
+
140
+ * Ruby 2.1.0 or later
141
+ * Nokogiri gem
142
+ * C compiler like gcc to install Nokogiri gem
143
+ * `patch` command to install Nokogiri gem
144
+
145
+ Install
146
+ -------
147
+
148
+ $ gem install nokogiri-xml-range
149
+
150
+ Copyright
151
+ ---------
152
+
153
+ Copyright (c) 2015 KITAITI Makoto
154
+
155
+ See {file:COPYING.txt} for details.
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ task :default => :test
7
+
8
+ begin
9
+ gem 'rubygems-tasks', '~> 0.2'
10
+ require 'rubygems/tasks'
11
+
12
+ Gem::Tasks.new
13
+ rescue LoadError => e
14
+ warn e.message
15
+ warn "Run `gem install rubygems-tasks` to install Gem::Tasks."
16
+ end
17
+
18
+ begin
19
+ gem 'yard', '~> 0.8'
20
+ require 'yard'
21
+
22
+ YARD::Rake::YardocTask.new
23
+ rescue LoadError => e
24
+ task :yard do
25
+ abort "Please run `gem install yard` to install YARD."
26
+ end
27
+ end
28
+ task :doc => :yard
29
+
30
+ require 'rake/testtask'
31
+ Rake::TestTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
@@ -0,0 +1,583 @@
1
+ # coding: utf-8
2
+ require 'nokogiri/xml/range/version'
3
+ require 'nokogiri'
4
+ require 'nokogiri/xml/range/refinement'
5
+ require 'nokogiri/xml/replacable'
6
+
7
+ using Nokogiri::XML::Range::Refinement
8
+
9
+ module Nokogiri::XML
10
+ class InvalidNodeTypeError < StandardError; end
11
+ class IndexSizeError < StandardError; end
12
+ class NotSupportedError < StandardError; end
13
+ class WrongDocumentError < StandardError; end
14
+ class HierarchyRequestError < StandardError; end
15
+ class NotFoundError < StandardError; end
16
+ class InvalidStateError < StandardError; end
17
+
18
+ class Range
19
+ START_TO_START = 0
20
+ START_TO_END = 1
21
+ END_TO_END = 2
22
+ END_TO_START = 3
23
+
24
+ class << self
25
+ def compare_points(node1, offset1, node2, offset2)
26
+ return unless node1.document == node2.document
27
+
28
+ case node1 <=> node2
29
+ when 0
30
+ offset1 <=> offset2
31
+ when 1
32
+ compare_points(node2, offset2, node1, offset1) * -1
33
+ else
34
+ ancestors = node2.ancestors_to(node1) # nil or [node2, parent of node2, ..., child of node1, node1]
35
+ if ancestors
36
+ child = nil
37
+ ancestors.reverse_each do |anc|
38
+ child = anc if anc.parent == node1
39
+ end
40
+ if node1.children.index(child) < offset1
41
+ 1
42
+ else
43
+ -1
44
+ end
45
+ else
46
+ -1
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ attr_reader :start_container, :start_offset, :end_container, :end_offset
53
+ alias start_node start_container
54
+ alias end_node end_container
55
+
56
+ def initialize(start_container, start_offset, end_container, end_offset)
57
+ @start_container, @start_offset, @end_container, @end_offset =
58
+ start_container, start_offset, end_container, end_offset
59
+ end
60
+
61
+ def start_point
62
+ [@start_container, @start_offset]
63
+ end
64
+
65
+ def end_point
66
+ [@end_container, @end_offset]
67
+ end
68
+
69
+ def document
70
+ @start_container.document
71
+ end
72
+
73
+ def root
74
+ document.root
75
+ end
76
+
77
+ def set_start(node, offset)
78
+ validate_boundary_point node, offset
79
+ if document != node.document or
80
+ self.class.compare_points(node, offset, @end_container, @end_offset) == 1
81
+ set_end node, offset
82
+ end
83
+ @start_container, @start_offset = node, offset
84
+ end
85
+ alias start= set_start
86
+
87
+ def set_end(node, offset)
88
+ validate_boundary_point node, offset
89
+ if document != node.document or
90
+ self.class.compare_points(node, offset, @start_container, @start_offset) == -1
91
+ set_start node, offset
92
+ end
93
+ @end_container, @end_offset = node, offset
94
+ end
95
+ alias end= set_end
96
+
97
+ def set_start_before(node)
98
+ parent = node.parent
99
+ raise InvalidNodeTypeError, 'parent node is empty' unless parent
100
+ set_start(parent, parent.children.index(node))
101
+ end
102
+
103
+ def set_start_after(node)
104
+ parent = node.parent
105
+ raise InvalidNodeTypeError, 'parent node is empty' unless parent
106
+ set_start(parent, parent.children.index(node) + 1)
107
+ end
108
+
109
+ def set_end_before(node)
110
+ parent = node.parent
111
+ raise InvalidNodeTypeError, 'parent node is empty' unless parent
112
+ set_end(parent, parent.children.index(node))
113
+ end
114
+
115
+ def set_end_after(node)
116
+ parent = node.parent
117
+ raise InvalidNodeTypeError, 'parent node is empty' unless parent
118
+ set_end(parent, parent.children.index(node) + 1)
119
+ end
120
+
121
+ def collapsed?
122
+ @start_offset == @end_offset and
123
+ @start_container == @end_container
124
+ end
125
+
126
+ def collapse(to_start=false)
127
+ to_start ? set_end(*start_point) : set_start(*end_point)
128
+ end
129
+ alias collapse! collapse
130
+
131
+ def common_ancestor_container
132
+ container = @start_container
133
+ ancestors_of_end = [@end_container] + @end_container.ancestors
134
+ until ancestors_of_end.include?(container)
135
+ container = container.parent
136
+ end
137
+ container
138
+ end
139
+
140
+ def select_node(node)
141
+ parent = node.parent
142
+ raise InvalidNodeTypeError, 'parent node is empty' unless parent
143
+ index = parent.children.index(node)
144
+ set_start parent, index
145
+ set_end parent, index + 1
146
+ end
147
+
148
+ def select_node_contents(node)
149
+ raise InvalidNodeTypeError, 'document type declaration is passed' if node.type == Node::DOCUMENT_TYPE_NODE
150
+ set_start node, 0
151
+ set_end node, node.length
152
+ end
153
+
154
+ def compare_boundary_points(how, source_range)
155
+ raise WrongDocumentError, 'different document' unless source_range.document == document
156
+ this_point, other_point =
157
+ case how
158
+ when START_TO_START
159
+ [start_point, source_range.start_point]
160
+ when START_TO_END
161
+ [end_point, source_range.start_point]
162
+ when END_TO_END
163
+ [end_point, source_range.end_point]
164
+ when END_TO_START
165
+ [start_point, source_range.end_point]
166
+ else
167
+ raise NotSupportedError, 'unsupported way to compare'
168
+ end
169
+ self.class.compare_points(this_point[0], this_point[1], other_point[0], other_point[1])
170
+ end
171
+
172
+ def delete_contents
173
+ return if collapsed?
174
+
175
+ if @start_container == @end_container and @start_container.replacable?
176
+ @start_container.replace_data @start_offset, @end_offset - @start_offset, ''
177
+ return
178
+ end
179
+
180
+ nodes_to_remove = NodeSet.new(document)
181
+ common_ancestor = common_ancestor_container
182
+ select_containing_children common_ancestor, nodes_to_remove
183
+
184
+ if @end_container.ancestors_to @start_container
185
+ new_node, new_offset = @start_container, @start_offset
186
+ else
187
+ reference_node = @start_container
188
+ parent = reference_node.parent
189
+ while parent and !@end_container.ancestors_to(parent)
190
+ reference_node = parent
191
+ parent = reference_node.parent
192
+ end
193
+ new_node = parent
194
+ new_offset = parent.children.index(reference_node) + 1
195
+ end
196
+
197
+ if @start_container.replacable?
198
+ @start_container.replace_data @start_offset, @start_container.length - @start_offset, ''
199
+ end
200
+
201
+ nodes_to_remove.each &:remove
202
+
203
+ if @end_container.replacable?
204
+ @end_container.replace_data 0, @end_offset, ''
205
+ end
206
+
207
+ @start_container = @end_container = new_node
208
+ @start_offset = @end_offset = new_offset
209
+ end
210
+
211
+ def extract_contents
212
+ fragment = DocumentFragment.new(document)
213
+ return fragment if collapsed?
214
+
215
+ if @start_container == @end_container and @start_container.replacable?
216
+ cloned = @start_container.clone(0)
217
+ cloned.content = @start_container.substring_data(@start_offset, @end_offset - @start_offset)
218
+ fragment << cloned
219
+ @start_container.replace_data @start_offset, @end_offset - @start_offset, ''
220
+ return fragment
221
+ end
222
+ common_ancestor = common_ancestor_container
223
+ end_node_ancestors = [@end_container] + @end_container.ancestors
224
+ first_partially_contained_child =nil
225
+ unless end_node_ancestors.include? @start_container
226
+ first_partially_contained_child = common_ancestor.children.find {|child|
227
+ partially_contain_node? child
228
+ }
229
+ end
230
+ last_partially_contained_child = nil
231
+ unless @start_container.ancestors_to @end_container
232
+ last_partially_contained_child = common_ancestor.children.reverse_each.find {|child|
233
+ partially_contain_node? child
234
+ }
235
+ end
236
+ contained_children = common_ancestor.children.select {|child|
237
+ contain_node? child
238
+ }
239
+ raise HierarchyRequestError if contained_children.any? {|child|
240
+ child.type == Node::DOCUMENT_TYPE_NODE
241
+ }
242
+
243
+ if end_node_ancestors.include? @start_container
244
+ new_node, new_offset = @start_container, @start_offset
245
+ else
246
+ reference_node = @start_container
247
+ parent = reference_node.parent
248
+ while parent and !end_node_ancestors.include?(parent)
249
+ reference_node = reference_node.parent
250
+ parent = reference_node.parent
251
+ end
252
+ new_node = parent
253
+ new_offset = parent.children.index(reference_node) + 1
254
+ end
255
+
256
+ if first_partially_contained_child && first_partially_contained_child.replacable?
257
+
258
+ cloned = @start_container.clone(0)
259
+ cloned.content = @start_container.substring_data(@start_offset, @start_container.length - @start_offset)
260
+ fragment << cloned
261
+ @start_container.replace_data @start_offset, @start_container.length - @start_offset, ''
262
+ elsif first_partially_contained_child
263
+ cloned = first_partially_contained_child.clone(0)
264
+ fragment << cloned
265
+ subrange = Range.new(@start_container, @start_offset, first_partially_contained_child, first_partially_contained_child.length)
266
+ subfragment = subrange.extract_contents
267
+ cloned << subfragment
268
+ end
269
+ contained_children.each do |contained_child|
270
+ fragment << contained_child
271
+ end
272
+ if last_partially_contained_child && last_partially_contained_child.replacable?
273
+
274
+ cloned = @end_container.clone(0)
275
+ cloned.content = @end_container.substring_data(0, @end_offset)
276
+ fragment << cloned
277
+ @end_container.replace_data 0, @end_offset, ''
278
+ elsif last_partially_contained_child
279
+ cloned = last_partially_contained_child.clone(0)
280
+ fragment << cloned
281
+ subrange = Range.new(last_partially_contained_child, 0, @end_container, @end_offset)
282
+ subfragment = subrange.extract_contents
283
+ cloned << subfragment
284
+ end
285
+ @start_container = @end_container = new_node
286
+ @start_offset = @end_offset = new_offset
287
+ fragment
288
+ end
289
+
290
+ def clone_contents
291
+ fragment = DocumentFragment.new(document)
292
+ return fragment if collapsed?
293
+
294
+ if @start_container == @end_container and @start_container.replacable?
295
+ cloned = @start_container.clone(0)
296
+ cloned.content = @start_container.substring_data(@start_offset, @end_offset - @start_offset)
297
+ fragment << cloned
298
+ return fragment
299
+ end
300
+
301
+ common_ancestor = common_ancestor_container
302
+ first_partially_contained_child = nil
303
+ @end_node_ancestors = [@end_container] + @end_container.ancestors
304
+ unless @end_node_ancestors.include?(@start_container)
305
+ first_partially_contained_child = common_ancestor.children.find {|child|
306
+ partially_contain_node? child
307
+ }
308
+ end
309
+ last_partially_contained_child = nil
310
+ unless ([@start_container] + @start_container.ancestors).include? @end_container
311
+ last_partially_contained_child = common_ancestor.children.reverse_each.find {|child|
312
+ partially_contain_node? child
313
+ }
314
+ end
315
+
316
+ contained_children = common_ancestor.children.select {|child|
317
+ contain_node? child
318
+ }
319
+
320
+ raise HierarchyRequestError if contained_children.any? {|child|
321
+ child.type == Node::DOCUMENT_TYPE_NODE
322
+ }
323
+
324
+ if first_partially_contained_child && first_partially_contained_child.replacable?
325
+
326
+ cloned = @start_container.clone(0)
327
+ cloned.content = @start_container.substring_data(@start_offset, @start_container.length - @start_offset)
328
+ fragment << cloned
329
+ elsif first_partially_contained_child
330
+ cloned =first_partially_contained_child.clone(0)
331
+ fragment << cloned
332
+ subrange = self.class.new(@start_container, @start_offset, first_partially_contained_child, first_partially_contained_child.length)
333
+ subfragment = subrange.clone_contents
334
+ cloned << subfragment
335
+ end
336
+
337
+ contained_children.each do |contained_child|
338
+ cloned = contained_child.clone(1)
339
+ fragment << cloned
340
+ end
341
+
342
+ if last_partially_contained_child && last_partially_contained_child.replacable?
343
+ cloned = @end_container.clone(0)
344
+ cloned.content = @end_container.substring_data(0, @end_offset)
345
+ fragment << cloned
346
+ elsif last_partially_contained_child
347
+ cloned = last_partially_contained_child.clone(0)
348
+ fragment << cloned
349
+ subrange = self.class.new(last_partially_contained_child, 0, @end_container, @end_offset)
350
+ subfragment = subrange.clone_contents
351
+ cloned << subfragment
352
+ end
353
+
354
+ fragment
355
+ end
356
+
357
+ def insert_node(node)
358
+ if [Node::PI_NODE, Node::COMMENT_NODE].include?(@start_container.type) or
359
+ @start_container.text? && @start_container.parent.nil?
360
+ raise HierarchyRequestError
361
+ end
362
+ reference_node = nil
363
+ if @start_container.text?
364
+ reference_node = @start_container
365
+ else
366
+ reference_node = @start_container.children[@start_offset]
367
+ end
368
+ if reference_node
369
+ parent = reference_node.parent
370
+ else
371
+ parent = @start_container
372
+ end
373
+ node.validate_pre_insertion parent, reference_node
374
+ if @start_container.text?
375
+ #7 reference_node = @start_container.split()
376
+ end
377
+ # Nokogiri doesn't support serial text node,
378
+ # so we need to handle it ourselves
379
+ split_node = nil
380
+ if @start_container.text?
381
+ split_node = self.class.new(@start_container, @start_offset, @start_container, @start_container.length).extract_contents
382
+ reference_node = split_node
383
+ end
384
+ if node == reference_node
385
+ reference_node = node.next_sibling
386
+ end
387
+ if node.parent
388
+ node.remove
389
+ end
390
+ if reference_node
391
+ if split_node
392
+ @start_container.parent.children.index(@start_container) + 1
393
+ else
394
+ new_offset = reference_node.parent.children.index(reference_node)
395
+ end
396
+ else
397
+ new_offset = parent.length
398
+ end
399
+
400
+ # pre-insert
401
+ if split_node
402
+ # pre-insert validation node parent reference_node(@start_container or split_node)
403
+ unless [Node::DOCUMENT_NODE, Node::DOCUMENT_FRAG_NODE, Node::ELEMENT_NODE].include? parent.type
404
+ raise HierarchyRequestError
405
+ end
406
+ raise hierarchyrequesterror if parent.host_including_inclusive_ancestor? node
407
+ raise Hierarchyrequesterror if reference_node and @start_container.parent != parent
408
+ unless [Node::DOCUMENT_FRAG_NODE, Node::DOCUMENT_TYPE_NODE, Node::ELEMENT_NODE, Node::TEXT_NODE, Node::PI_NODE, Node::COMMENT_NODE].include? node.type
409
+ raise Hierarchyrequesterror
410
+ end
411
+ raise HierarchyRequestError if node.text? && parent.document?
412
+ raise HierarchyRequestError if node.type == Node::DOCUMENT_TYPE_NODE and !parent.document?
413
+ if parent.document?
414
+ case node.type
415
+ when Node::DOCUMENT_FRAG_NODE
416
+ child_element_count = 0
417
+ node.children.each do |n|
418
+ raise HierarchyRequestError if n.text?
419
+ child_element_count += 1 if n.element?
420
+ raise HierarchyRequestError if child_element_count > 1
421
+ end
422
+ if child_element_count == 1
423
+ raise HierarchyRequestError if parent.children.any?(&:element?)
424
+ if reference_node
425
+ raise HierarchyRequestError if reference_node.type == Node::DOCUMENT_TYPE_NODE
426
+ raise HierarchyRequestError if @start_container.following_node.type == Node::DOCUMENT_TYPE_NODE
427
+ end
428
+ end
429
+ when Node::ELEMENT_NODE
430
+ raise HierarchyRequestError if parent.children.any?(&:element?)
431
+ if reference_node
432
+ raise HierarchyRequestError if reference_node.child == Node::DOCUMENT_TYPE_NODE
433
+ raise HierarchyRequestError if @start_container.following_node.type == Node::DOCUMENT_TYPE_NODE
434
+ end
435
+ when Node::DOCUMENT_TYPE_NODE
436
+ raise HierarchyRequestError if parent.children.any? {|n|
437
+ n.type == Node::DOCUMENT_TYPE_NODE
438
+ }
439
+ if reference_node
440
+ raise HierarchyRequestError if @start_container.preceding_node.element?
441
+ raise HierarchyRequestError if parent.children.any?(&:element?)
442
+ end
443
+ end
444
+ end
445
+
446
+ reference_child = @start_container
447
+ if reference_child == parent
448
+ reference_child = split_node
449
+ end
450
+ parent.document.adopt node
451
+ @start_container.after node
452
+ node.after split_node
453
+ else
454
+ node.validate_pre_insertion parent, reference_node
455
+ reference_child = reference_node
456
+ if reference_child == parent
457
+ reference_child = parent.next_sibling
458
+ end
459
+ parent.document.adopt node
460
+ reference_child.before node
461
+ end
462
+
463
+ if collapsed?
464
+ @end_container, @end_offset = parent, new_offset
465
+ end
466
+ end
467
+
468
+ def surround_contents(new_parent)
469
+ raise InvalidStateError unless partially_containing_nodes.all?(&:text?)
470
+ raise InvalidNodeTypeError if [Node::DOCUMENT_NODE, Node::DOCUMENT_TYPE_NODE, Node::DOCUMENT_FRAG_NODE].include? new_parent.type
471
+ fragment = extract_contents
472
+ new_parent.replace_all_with nil if new_parent.child
473
+ insert_node new_parent
474
+ new_parent << fragment
475
+ select_node new_parent
476
+ end
477
+
478
+ def contain_node?(node)
479
+ document == node.document and
480
+ self.class.compare_points(node, 0, @start_container, @start_offset) == 1 and
481
+ self.class.compare_points(node, node.length, @end_container, @end_offset) == -1
482
+ end
483
+ alias include_node? contain_node?
484
+ alias cover_node? contain_node?
485
+
486
+ def containing_nodes
487
+ nodes = NodeSet.new(document)
488
+ select_containing_nodes common_ancestor_container, nodes
489
+
490
+ nodes
491
+ end
492
+
493
+ def partially_contain_node?(node)
494
+ path_to_start = @start_container.ancestors_to(node)
495
+ path_to_end = @end_container.ancestors_to(node)
496
+ !path_to_start.nil? && path_to_end.nil? or
497
+ path_to_start.nil? && !path_to_end.nil?
498
+ end
499
+ alias partially_include_node? partially_contain_node?
500
+ alias partially_cover_node? partially_contain_node?
501
+
502
+ def partially_containing_nodes
503
+ inclusive_ancestors_of_start = @start_container.inclusive_ancestors
504
+ inclusive_ancestors_of_end = @end_container.inclusive_ancestors
505
+ (inclusive_ancestors_of_start | inclusive_ancestors_of_end) -
506
+ (inclusive_ancestors_of_start & inclusive_ancestors_of_end)
507
+ end
508
+
509
+ def point_in_range?(node, offset)
510
+ return false unless node.ancestors.last == @start_container.ancestors.last
511
+ raise InvalidNodeTypeError if node.type == Node::DOCUMENT_TYPE_NODE
512
+ raise IndexSizeError if offset > node.length
513
+ return false if self.class.compare_points(node, offset, @start_container, @start_offset) == -1
514
+ return false if self.class.compare_points(node, offset, @end_container, @end_offset) == 1
515
+ true
516
+ end
517
+
518
+ def compare_point(node, offset)
519
+ raise WrongDocumentError unless node.ancestors.last == @start_container.ancestors.last
520
+ raise InvalidNodeTypeError if node.type == Node::DOCUMENT_TYPE_NODE
521
+ raise IndexSizeError if offset > node.length
522
+ return -1 if self.class.compare_points(node, offset, @start_container, @start_offset) == -1
523
+ return 1 if self.class.compare_points(node, offset, @end_container, @end_offset) == 1
524
+ 0
525
+ end
526
+
527
+ def intersect_node?(node)
528
+ return false unless node.ancestors.last == @start_container.ancestors.last
529
+ return true unless node.respond_to?(:parent)
530
+ parent = node.parent
531
+ return true unless parent
532
+ offset = parent.children.index(node)
533
+ (self.class.compare_points(parent, offset, @end_container, @end_offset) == -1) and
534
+ (self.class.compare_points(parent, offset + 1, @start_container, @start_offset) == 1)
535
+ end
536
+
537
+ def to_s
538
+ s = ''
539
+ if @start_container == @end_container and @start_container.text?
540
+ return @start_container.substring_data(@start_offset, @end_offset - @start_offset)
541
+ end
542
+ if @start_container.text?
543
+ s << @start_container.substring_data(@start_offset, @start_container.length)
544
+ end
545
+ containing_nodes.reduce s do |concatenated, node|
546
+ concatenated << node.content if node.text?
547
+ concatenated
548
+ end
549
+ if @end_container.text?
550
+ s << @end_container.substring_data(0, @end_offset)
551
+ end
552
+ s
553
+ end
554
+
555
+ private
556
+
557
+ def validate_boundary_point(node, offset)
558
+ raise InvalidNodeTypeError, 'document type declaration cannot be a boundary point' if node.type == Node::DOCUMENT_TYPE_NODE
559
+ raise IndexSizeError, 'offset is greater than node length' if offset > node.length
560
+ end
561
+
562
+ # @note depth first order
563
+ # @note modifies +node_set+
564
+ def select_containing_children(node, node_set)
565
+ if contain_node?(node)
566
+ node_set << node
567
+ else
568
+ node.children.each do |child|
569
+ select_containing_children child, node_set
570
+ end
571
+ end
572
+ end
573
+
574
+ # @note depth first order
575
+ # @note modifies +node_set+
576
+ def select_containing_nodes(node, node_set)
577
+ node_set << node if contain_node?(node)
578
+ node.children.each do |child|
579
+ select_containing_nodes child, node_set
580
+ end
581
+ end
582
+ end
583
+ end