4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * This class logic for filtering text and matching words.
14 * @class tinymce.spellcheckerplugin.TextFilter
17 define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() {
18 // Based on work developed by: James Padolsey http://james.padolsey.com
19 // released under UNLICENSE that is compatible with LGPL
20 // TODO: Handle contentEditable edgecase:
21 // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
22 return function(regex, node, schema) {
23 var m, matches = [], text, count = 0, doc;
24 var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
26 doc = node.ownerDocument;
27 blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
28 hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
29 shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
31 function getMatchIndexes(m) {
33 throw 'findAndReplaceDOMText cannot handle zero-length matches';
38 return [index, index + m[0].length, [m[0]]];
41 function getText(node) {
44 if (node.nodeType === 3) {
48 if (hiddenTextElementsMap[node.nodeName]) {
54 if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
58 if ((node = node.firstChild)) {
61 } while ((node = node.nextSibling));
67 function stepThroughMatches(node, matches, replaceFn) {
68 var startNode, endNode, startNodeIndex,
69 endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
70 matchLocation = matches.shift(), matchIndex = 0;
73 if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
77 if (curNode.nodeType === 3) {
78 if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
79 // We've found the ending
81 endNodeIndex = matchLocation[1] - atIndex;
82 } else if (startNode) {
84 innerNodes.push(curNode);
87 if (!startNode && curNode.length + atIndex > matchLocation[0]) {
88 // We've found the match start
90 startNodeIndex = matchLocation[0] - atIndex;
93 atIndex += curNode.length;
96 if (startNode && endNode) {
99 startNodeIndex: startNodeIndex,
101 endNodeIndex: endNodeIndex,
102 innerNodes: innerNodes,
103 match: matchLocation[2],
104 matchIndex: matchIndex
107 // replaceFn has to return the node that replaced the endNode
108 // and then we step back so we can continue from the end of the
110 atIndex -= (endNode.length - endNodeIndex);
114 matchLocation = matches.shift();
117 if (!matchLocation) {
118 break; // no more matches
120 } else if (!hiddenTextElementsMap[curNode.nodeName] && curNode.firstChild) {
122 curNode = curNode.firstChild;
124 } else if (curNode.nextSibling) {
126 curNode = curNode.nextSibling;
130 // Move forward or up:
132 if (curNode.nextSibling) {
133 curNode = curNode.nextSibling;
135 } else if (curNode.parentNode !== node) {
136 curNode = curNode.parentNode;
145 * Generates the actual replaceFn which splits up text nodes
146 * and inserts the replacement element.
148 function genReplacer(nodeName) {
149 var makeReplacementNode;
151 if (typeof nodeName != 'function') {
152 var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
154 makeReplacementNode = function(fill, matchIndex) {
155 var clone = stencilNode.cloneNode(false);
157 clone.setAttribute('data-mce-index', matchIndex);
160 clone.appendChild(doc.createTextNode(fill));
166 makeReplacementNode = nodeName;
169 return function replace(range) {
170 var before, after, parentNode, startNode = range.startNode,
171 endNode = range.endNode, matchIndex = range.matchIndex;
173 if (startNode === endNode) {
174 var node = startNode;
176 parentNode = node.parentNode;
177 if (range.startNodeIndex > 0) {
178 // Add `before` text node (before the match)
179 before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
180 parentNode.insertBefore(before, node);
183 // Create the replacement node:
184 var el = makeReplacementNode(range.match[0], matchIndex);
185 parentNode.insertBefore(el, node);
186 if (range.endNodeIndex < node.length) {
187 // Add `after` text node (after the match)
188 after = doc.createTextNode(node.data.substring(range.endNodeIndex));
189 parentNode.insertBefore(after, node);
192 node.parentNode.removeChild(node);
196 // Replace startNode -> [innerNodes...] -> endNode (in that order)
197 before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
198 after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
199 var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
202 for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
203 var innerNode = range.innerNodes[i];
204 var innerEl = makeReplacementNode(innerNode.data, matchIndex);
205 innerNode.parentNode.replaceChild(innerEl, innerNode);
206 innerEls.push(innerEl);
209 var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
211 parentNode = startNode.parentNode;
212 parentNode.insertBefore(before, startNode);
213 parentNode.insertBefore(elA, startNode);
214 parentNode.removeChild(startNode);
216 parentNode = endNode.parentNode;
217 parentNode.insertBefore(elB, endNode);
218 parentNode.insertBefore(after, endNode);
219 parentNode.removeChild(endNode);
226 text = getText(node);
227 if (text && regex.global) {
228 while ((m = regex.exec(text))) {
229 matches.push(getMatchIndexes(m));
233 function filter(callback) {
234 var filteredMatches = [];
236 each(function(match, i) {
237 if (callback(match, i)) {
238 filteredMatches.push(match);
242 matches = filteredMatches;
244 /*jshint validthis:true*/
248 function each(callback) {
249 for (var i = 0, l = matches.length; i < l; i++) {
250 if (callback(matches[i], i) === false) {
255 /*jshint validthis:true*/
259 function mark(replacementNode) {
260 if (matches.length) {
261 count = matches.length;
262 stepThroughMatches(node, matches, genReplacer(replacementNode));
265 /*jshint validthis:true*/