nokogiri-xml-range 0.1.0

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