1 | /*
|
---|
2 | * file: ContainerNormalization.js
|
---|
3 | *
|
---|
4 | * @BEGINLICENSE
|
---|
5 | * Copyright 2010 Brook Novak (email : [email protected])
|
---|
6 | * This program is free software; you can redistribute it and/or modify
|
---|
7 | * it under the terms of the GNU General Public License as published by
|
---|
8 | * the Free Software Foundation; either version 2 of the License, or
|
---|
9 | * (at your option) any later version.
|
---|
10 | * This program is distributed in the hope that it will be useful,
|
---|
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
---|
13 | * GNU General Public License for more details.
|
---|
14 | * You should have received a copy of the GNU General Public License
|
---|
15 | * along with this program; if not, write to the Free Software
|
---|
16 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
---|
17 | * @ENDLICENSE
|
---|
18 | */
|
---|
19 | bootstrap.provides("ContainerNormalization");
|
---|
20 |
|
---|
21 | /*
|
---|
22 | * TODO: MAKE HTML VALID BY PLACING INLINE ANCESTORS WITHIN THE BLOCK-LEVEL CONTAINERS
|
---|
23 | * Inlines cannot contain blocks
|
---|
24 | *
|
---|
25 | *
|
---|
26 | * ARGGG... OK JUST RUBBISH THIS.. For all invalid inlines, remove them from the document.. that will naturally
|
---|
27 | * "normalize" the range. Cannot just move inlines into containers because containers may contain block decendants.
|
---|
28 | *
|
---|
29 | * This will simplify the code a lot and keep the HTML tidy.
|
---|
30 | *
|
---|
31 | * Should design algrorithms to assume valid HTML, ad if invalid then loss of formatting as a result of throwing away
|
---|
32 | * invalid tags doesnt matter.
|
---|
33 | */
|
---|
34 |
|
---|
35 | /**
|
---|
36 | * Creates containers, adjusts structure within the given range, such that
|
---|
37 | * it garauntees that all block-level elements in the range do not share any ancestor inline elements
|
---|
38 | * that occur between the containers and the common-block-level-ancestor,
|
---|
39 | * with other block-level elements. This property is useful for itemizing container in
|
---|
40 | * a given range.
|
---|
41 | * <br>
|
---|
42 | * This operation is undoable, and is intended for indentation and itemizing ranges.
|
---|
43 | *
|
---|
44 | * @param {Node} startNode The starting dom node of the range to normalize.
|
---|
45 | *
|
---|
46 | * @param {Node} endNode The ending dom node of the range to normalize.
|
---|
47 | * Can be the same as start node or it must occur
|
---|
48 | * after the start dom (in-order left-to-right traversal)
|
---|
49 | *
|
---|
50 | * @param {Node} containerTemplate (Optional) An element which supports inline elements to be
|
---|
51 | * used for inline-groups which need containers. Defaults to
|
---|
52 | * paragraph.
|
---|
53 | *
|
---|
54 | * @return {[Node]} An array containing a list of all the top-level containers in the given range,
|
---|
55 | * in order if traversing the dom tree in-order. Can be empty.
|
---|
56 | *
|
---|
57 | */
|
---|
58 | function _getNormalizedContainerRange (startNode, endNode, containerTemplate) {
|
---|
59 |
|
---|
60 | var template = containerTemplate || $createElement("p");
|
---|
61 |
|
---|
62 | // Determine the master container for the normalization range
|
---|
63 | var masterContainer = _findAncestor(_getCommonAncestor(startNode, endNode, true),
|
---|
64 | docBody,
|
---|
65 | _isBlockLevel,
|
---|
66 | true)
|
---|
67 | || docBody;
|
---|
68 |
|
---|
69 | // Deepen start/end range
|
---|
70 | while (startNode.firstChild) {
|
---|
71 | startNode = startNode.firstChild;
|
---|
72 | }
|
---|
73 | while (endNode.lastChild) {
|
---|
74 | endNode = endNode.lastChild;
|
---|
75 | }
|
---|
76 |
|
---|
77 | // Check if the master container allows the container template
|
---|
78 | if (masterContainer != docBody && !_isValidRelationship(template, masterContainer)) {
|
---|
79 |
|
---|
80 | // Special case - list items
|
---|
81 | if (_nodeName(masterContainer) == "ul" || _nodeName(masterContainer) == "ol") {
|
---|
82 | startNode = _findAncestor(startNode, masterContainer);
|
---|
83 | endNode = _findAncestor(endNode, masterContainer);
|
---|
84 | var containers = [];
|
---|
85 | while (startNode) {
|
---|
86 | if (_nodeName(startNode) == "li")
|
---|
87 | containers.push(startNode);
|
---|
88 | startNode = startNode == endNode ? null : startNode.nextSibling;
|
---|
89 | }
|
---|
90 | return containers;
|
---|
91 |
|
---|
92 | }
|
---|
93 |
|
---|
94 | // If the master container doesn't allow sub-containers, then the normalized range
|
---|
95 | // becomes a single container... the master container.
|
---|
96 | return [masterContainer]
|
---|
97 | }
|
---|
98 |
|
---|
99 | // Extend range to point to begin and start at top-level sub-containers if they exist,
|
---|
100 | // or to the end of inline groups
|
---|
101 | var extendedRangeNode,
|
---|
102 | protectedContainers = [],
|
---|
103 | reinsertProcContainers = [],
|
---|
104 | extendedStart,
|
---|
105 | extendedEnd;
|
---|
106 |
|
---|
107 | // Extend start range
|
---|
108 | _visitAllNodes(masterContainer, startNode, false, extendRange);
|
---|
109 |
|
---|
110 | // Remove any protect containers
|
---|
111 | if (protectedContainers.length > 0) {
|
---|
112 | for (var i in protectedContainers) {
|
---|
113 | if (protectedContainers[i].parentNode) {
|
---|
114 | protectedContainers[i].parentNode.removeChild(protectedContainers[i]);
|
---|
115 | reinsertProcContainers.push(protectedContainers[i]);
|
---|
116 | }
|
---|
117 | }
|
---|
118 |
|
---|
119 | }
|
---|
120 |
|
---|
121 | extendedStart = extendedRangeNode;
|
---|
122 | if (!extendedStart) {
|
---|
123 | // Set start through to end of initial inline group
|
---|
124 | extendedStart = masterContainer.firstChild;
|
---|
125 | while (extendedStart.firstChild) {
|
---|
126 | extendedStart = extendedStart.firstChild;
|
---|
127 | }
|
---|
128 | }
|
---|
129 |
|
---|
130 | // Extend end range
|
---|
131 | extendedRangeNode = null;
|
---|
132 | protectedContainers = [];
|
---|
133 | _visitAllNodes(masterContainer, endNode, true, extendRange);
|
---|
134 |
|
---|
135 | // Remove any protect containers
|
---|
136 | if (protectedContainers.length > 0) {
|
---|
137 | for (var i in protectedContainers) {
|
---|
138 | if (protectedContainers[i].parentNode) {
|
---|
139 | protectedContainers[i].parentNode.removeChild(protectedContainers[i]);
|
---|
140 | reinsertProcContainers.push(protectedContainers[i]);
|
---|
141 | }
|
---|
142 | }
|
---|
143 | }
|
---|
144 |
|
---|
145 | extendedEnd = extendedRangeNode;
|
---|
146 | if (!extendedEnd) {
|
---|
147 | // Set start through to end of post inline group
|
---|
148 | extendedEnd = masterContainer.lastChild;
|
---|
149 | while (extendedEnd.lastChild) {
|
---|
150 | extendedEnd = extendedEnd.lastChild;
|
---|
151 | }
|
---|
152 | }
|
---|
153 |
|
---|
154 | // Move protected nodes out of the extended range if there are any
|
---|
155 | for (var i in reinsertProcContainers) {
|
---|
156 | docBody.appendChild(reinsertProcContainers[i]);
|
---|
157 | }
|
---|
158 |
|
---|
159 | // The range only containers protected nodes
|
---|
160 | if (!extendedEnd || ! extendedStart)
|
---|
161 | return [];
|
---|
162 |
|
---|
163 | // Perform normalization operations (two phases)
|
---|
164 | separateBlockPaths(extendedStart, extendedEnd, masterContainer);
|
---|
165 | encapsulateIGroups(extendedStart, extendedEnd, masterContainer, template);
|
---|
166 |
|
---|
167 | // Get top-level containers within the original range
|
---|
168 | var tdc = _findAncestor(startNode, masterContainer);
|
---|
169 | endNode = _findAncestor(endNode, masterContainer);
|
---|
170 |
|
---|
171 | var range = [];
|
---|
172 |
|
---|
173 | do {
|
---|
174 | var node = tdc;
|
---|
175 | while (node) {
|
---|
176 | if (_isBlockLevel(node)) {
|
---|
177 | range.push(node);
|
---|
178 | break;
|
---|
179 | }
|
---|
180 | node = node.firstChild;
|
---|
181 | }
|
---|
182 | tdc = tdc == endNode ? null : tdc.nextSibling;
|
---|
183 | } while (tdc);
|
---|
184 |
|
---|
185 | return range;
|
---|
186 |
|
---|
187 | /**
|
---|
188 | * Helper function for extending the range backward/forward.
|
---|
189 | * Sets the extendedRangeNode local iff a top-level block container is found within the master container
|
---|
190 | *
|
---|
191 | * @param {Node} domNode Provided by de.visit function
|
---|
192 | */
|
---|
193 | function extendRange(domNode) {
|
---|
194 | if (domNode == masterContainer)
|
---|
195 | return;
|
---|
196 |
|
---|
197 | // Find top-level block element from the master-container - inclusive of self
|
---|
198 | var node = domNode, blNode = null;
|
---|
199 | while (node != masterContainer) {
|
---|
200 | if (_isBlockLevel(node))
|
---|
201 | blNode = node;
|
---|
202 |
|
---|
203 | // If this sub-tree is protected then note the protected node root
|
---|
204 | if (node.parentNode == masterContainer && de.doc.getProtectedNodeContainer(node) == node) {
|
---|
205 | protectedContainers.push(node);
|
---|
206 | blNode = null; // Ignore this since this subtree will be repositioned in the document
|
---|
207 | }
|
---|
208 |
|
---|
209 | node = node.parentNode;
|
---|
210 | }
|
---|
211 |
|
---|
212 | if (blNode) {
|
---|
213 | // If this node has a top-level block element from the master-container
|
---|
214 | // then the extended start has been located
|
---|
215 | extendedRangeNode = blNode;
|
---|
216 | return false;
|
---|
217 | }
|
---|
218 | }
|
---|
219 |
|
---|
220 |
|
---|
221 | /**
|
---|
222 | * Ensures that all first occuring block-level elements from the master container own their own path
|
---|
223 | * up to the master container.
|
---|
224 | *
|
---|
225 | * @param {Node} startNode The start node in the range. This must not have a block-level element
|
---|
226 | * in the path from itself up to the master container (exclusive).
|
---|
227 | *
|
---|
228 | * @param {Node} endNode The end node in the range.
|
---|
229 | *
|
---|
230 | * @param {Node} masterContainer The master container for all sub containers
|
---|
231 | *
|
---|
232 | */
|
---|
233 | function separateBlockPaths(startNode, endNode, masterContainer) {
|
---|
234 |
|
---|
235 | var scanPoint = startNode,
|
---|
236 | seenEndPoint = false;
|
---|
237 |
|
---|
238 | // Scan through range for all top-level block elements from the master container
|
---|
239 | while (scanPoint && !seenEndPoint) {
|
---|
240 |
|
---|
241 | _visitAllNodes(masterContainer, scanPoint, true, function(domNode) {
|
---|
242 |
|
---|
243 | scanPoint = null;
|
---|
244 | seenEndPoint |= domNode == endNode;
|
---|
245 |
|
---|
246 | if (domNode == masterContainer)
|
---|
247 | return;
|
---|
248 |
|
---|
249 | // If this dom node is a block element then ensure that it owns its own path
|
---|
250 | // up to the master container.
|
---|
251 | if (_isBlockLevel(domNode)) {
|
---|
252 |
|
---|
253 | // Note: this block level element is top-level from the master container... i.e. there is no
|
---|
254 | // block-level elements between this node up to the master container
|
---|
255 |
|
---|
256 | var pivotNode = domNode;
|
---|
257 | while (pivotNode.parentNode != masterContainer) {
|
---|
258 |
|
---|
259 | // Separate previous siblings into duplicate inline node
|
---|
260 | if (pivotNode.previousSibling) {
|
---|
261 |
|
---|
262 | // Clone shared inline element
|
---|
263 | var clone = pivotNode.parentNode.cloneNode(false);
|
---|
264 |
|
---|
265 | // Migrate previous siblings into duped inline element
|
---|
266 | while (pivotNode.previousSibling) {
|
---|
267 |
|
---|
268 | var psib = pivotNode.previousSibling;
|
---|
269 |
|
---|
270 | _execOp(_Operation.REMOVE_NODE, psib);
|
---|
271 | _execOp(_Operation.INSERT_NODE, psib, clone, 0);
|
---|
272 |
|
---|
273 | }
|
---|
274 |
|
---|
275 | // Insert the cloned inline element back into the document
|
---|
276 | _execOp(_Operation.INSERT_NODE, clone, pivotNode.parentNode.parentNode, _indexInParent(pivotNode.parentNode));
|
---|
277 |
|
---|
278 | }
|
---|
279 |
|
---|
280 | // Separate next siblings into duplicate inline node
|
---|
281 | if (pivotNode.nextSibling) {
|
---|
282 |
|
---|
283 | // Clone shared inline element
|
---|
284 | var clone = pivotNode.parentNode.cloneNode(false)
|
---|
285 |
|
---|
286 | // Migrate next siblings into duped inline element
|
---|
287 | while (pivotNode.nextSibling) {
|
---|
288 | var nsib = pivotNode.nextSibling;
|
---|
289 | _execOp(_Operation.REMOVE_NODE, nsib);
|
---|
290 | _execOp(_Operation.INSERT_NODE, nsib, clone);
|
---|
291 | }
|
---|
292 |
|
---|
293 | // Insert the cloned inline element back into the document
|
---|
294 | _execOp(_Operation.INSERT_NODE, clone, pivotNode.parentNode.parentNode, _indexInParent(pivotNode.parentNode) + 1);
|
---|
295 |
|
---|
296 | }
|
---|
297 |
|
---|
298 | pivotNode = pivotNode.parentNode;
|
---|
299 |
|
---|
300 | } // End separating path to master container
|
---|
301 |
|
---|
302 | // This dom node now owns its own path to the master container.
|
---|
303 | // Setup to scan down next path
|
---|
304 | scanPoint = pivotNode.nextSibling; // NB: Pivot is an immediate child of master container
|
---|
305 |
|
---|
306 | // Check if the end node lies within this block-level element
|
---|
307 | _visitAllNodes(domNode, domNode, true, function(subDomNode) {
|
---|
308 | if (subDomNode == endNode)
|
---|
309 | seenEndPoint = true;
|
---|
310 | return !seenEndPoint;
|
---|
311 | });
|
---|
312 |
|
---|
313 | return false;
|
---|
314 | }
|
---|
315 |
|
---|
316 | return !seenEndPoint;
|
---|
317 |
|
---|
318 | });
|
---|
319 |
|
---|
320 | } // End Loop: separating paths to block-level elements in given range
|
---|
321 |
|
---|
322 | } // End function separateBlockPaths
|
---|
323 |
|
---|
324 |
|
---|
325 | /**
|
---|
326 | * For all sub-trees within the given range of the master container (i.e. Immediate children of the master continer),
|
---|
327 | * all sub-trees containing only inline elements/text nodes are migrated into a containerTemplate clone
|
---|
328 | * within the master container.
|
---|
329 | *
|
---|
330 | *
|
---|
331 | * @param {Node} startNode The start node to encapsulate from. This should not be within an inline sub-tree
|
---|
332 | * which has a previous sub-tree which contains only inline elements.
|
---|
333 | *
|
---|
334 | * @param {Node} endNode The end node to encapsulate to. This should not be within an inline sub-tree
|
---|
335 | * which has a following sub-tree which contains only inline elements.
|
---|
336 | *
|
---|
337 | * @param {Node} masterContainer The master container
|
---|
338 | *
|
---|
339 | * @param {Node} containerTemplate The container to clone for migrating inline groups into
|
---|
340 | *
|
---|
341 | */
|
---|
342 | function encapsulateIGroups(startNode, endNode, masterContainer, containerTemplate) {
|
---|
343 |
|
---|
344 | // Adjust start and end nodes to their root nodes within the master container
|
---|
345 | startNode = _findAncestor(startNode, masterContainer);
|
---|
346 | endNode = _findAncestor(endNode, masterContainer);
|
---|
347 |
|
---|
348 | var subTreeNode = startNode,
|
---|
349 | inlineGroupStart = null;
|
---|
350 |
|
---|
351 | // Visit all sub-trees in the master container within the range
|
---|
352 | // (i.e. immediate children in the master contianer).
|
---|
353 | while (subTreeNode) {
|
---|
354 |
|
---|
355 | var domNode = subTreeNode,
|
---|
356 | containsBlock = false;
|
---|
357 |
|
---|
358 | while (domNode) {
|
---|
359 | if (_isBlockLevel(domNode)) {
|
---|
360 | containsBlock = true;
|
---|
361 | break;
|
---|
362 | }
|
---|
363 | // Just check down left-most-path since all block level elements in
|
---|
364 | // the given range have their own path to the master container
|
---|
365 | domNode = domNode.firstChild;
|
---|
366 | }
|
---|
367 |
|
---|
368 | if (containsBlock) {
|
---|
369 |
|
---|
370 | if (inlineGroupStart) {
|
---|
371 | encapsulate(inlineGroupStart, subTreeNode);
|
---|
372 | inlineGroupStart = null;
|
---|
373 | }
|
---|
374 |
|
---|
375 | } else {
|
---|
376 | // Mark start of inline group - avoid including whitespace nodes which should not be encapsulated.
|
---|
377 | // For example, whitespace in between list items
|
---|
378 | if (!inlineGroupStart) {
|
---|
379 | if (!(subTreeNode.nodeType == Node.TEXT_NODE && !_doesTextSupportNonWS(subTreeNode)))
|
---|
380 | inlineGroupStart = subTreeNode;
|
---|
381 | }
|
---|
382 | }
|
---|
383 |
|
---|
384 | subTreeNode = subTreeNode == endNode ? null : subTreeNode.nextSibling;
|
---|
385 | }
|
---|
386 |
|
---|
387 | if (inlineGroupStart)
|
---|
388 | encapsulate(inlineGroupStart, null);
|
---|
389 |
|
---|
390 | /**
|
---|
391 | * Encapsulates a run of inline siblings within the master container.
|
---|
392 | *
|
---|
393 | * @param {Node} start The sibling to start encapsulating from.
|
---|
394 | *
|
---|
395 | * @param {Node} endEx The exclusive end sibling.
|
---|
396 | * Can be null for encapsulating all siblings from start node.
|
---|
397 | */
|
---|
398 | function encapsulate(start, endEx) {
|
---|
399 |
|
---|
400 | var igContainer = containerTemplate.cloneNode(false),
|
---|
401 | igSubTree = start;
|
---|
402 |
|
---|
403 | // Insert the new inline-group container into the master container
|
---|
404 | debug.assert(_isValidRelationship(igContainer, masterContainer));
|
---|
405 | _execOp(_Operation.INSERT_NODE, igContainer, masterContainer, _indexInParent(start));
|
---|
406 |
|
---|
407 | // Migrate inline run into the new container
|
---|
408 | while (igSubTree != endEx) {
|
---|
409 | var nextSib = igSubTree.nextSibling;
|
---|
410 | _execOp(_Operation.REMOVE_NODE, igSubTree);
|
---|
411 | _execOp(_Operation.INSERT_NODE, igSubTree, igContainer);
|
---|
412 | igSubTree = nextSib;
|
---|
413 | }
|
---|
414 | }
|
---|
415 |
|
---|
416 | } // End function encapsulateIGroups
|
---|
417 |
|
---|
418 |
|
---|
419 | } // End getNormalizedContainerRange
|
---|
420 |
|
---|
421 |
|
---|
422 |
|
---|
423 |
|
---|
424 |
|
---|