diff -r 000000000000 -r bca7a7e058a3 src/pyams_skin/resources/js/ext/tinymce/dev/plugins/spellchecker/classes/DomTextMatcher.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_skin/resources/js/ext/tinymce/dev/plugins/spellchecker/classes/DomTextMatcher.js Thu Feb 13 11:43:31 2020 +0100 @@ -0,0 +1,470 @@ +/** + * DomTextMatcher.js + * + * Copyright, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/*eslint no-labels:0, no-constant-condition: 0 */ + +/** + * This class logic for filtering text and matching words. + * + * @class tinymce.spellcheckerplugin.TextFilter + * @private + */ +define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() { + // Based on work developed by: James Padolsey http://james.padolsey.com + // released under UNLICENSE that is compatible with LGPL + // TODO: Handle contentEditable edgecase: + //

texttexttexttexttext

+ return function(node, editor) { + var m, matches = [], text, dom = editor.dom; + var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap; + + blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc + hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT + shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT + + function createMatch(m, data) { + if (!m[0]) { + throw 'findAndReplaceDOMText cannot handle zero-length matches'; + } + + return { + start: m.index, + end: m.index + m[0].length, + text: m[0], + data: data + }; + } + + function getText(node) { + var txt; + + if (node.nodeType === 3) { + return node.data; + } + + if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) { + return ''; + } + + txt = ''; + + if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) { + txt += '\n'; + } + + if ((node = node.firstChild)) { + do { + txt += getText(node); + } while ((node = node.nextSibling)); + } + + return txt; + } + + function stepThroughMatches(node, matches, replaceFn) { + var startNode, endNode, startNodeIndex, + endNodeIndex, innerNodes = [], atIndex = 0, curNode = node, + matchLocation, matchIndex = 0; + + matches = matches.slice(0); + matches.sort(function(a, b) { + return a.start - b.start; + }); + + matchLocation = matches.shift(); + + out: while (true) { + if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) { + atIndex++; + } + + if (curNode.nodeType === 3) { + if (!endNode && curNode.length + atIndex >= matchLocation.end) { + // We've found the ending + endNode = curNode; + endNodeIndex = matchLocation.end - atIndex; + } else if (startNode) { + // Intersecting node + innerNodes.push(curNode); + } + + if (!startNode && curNode.length + atIndex > matchLocation.start) { + // We've found the match start + startNode = curNode; + startNodeIndex = matchLocation.start - atIndex; + } + + atIndex += curNode.length; + } + + if (startNode && endNode) { + curNode = replaceFn({ + startNode: startNode, + startNodeIndex: startNodeIndex, + endNode: endNode, + endNodeIndex: endNodeIndex, + innerNodes: innerNodes, + match: matchLocation.text, + matchIndex: matchIndex + }); + + // replaceFn has to return the node that replaced the endNode + // and then we step back so we can continue from the end of the + // match: + atIndex -= (endNode.length - endNodeIndex); + startNode = null; + endNode = null; + innerNodes = []; + matchLocation = matches.shift(); + matchIndex++; + + if (!matchLocation) { + break; // no more matches + } + } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) { + // Move down + curNode = curNode.firstChild; + continue; + } else if (curNode.nextSibling) { + // Move forward: + curNode = curNode.nextSibling; + continue; + } + + // Move forward or up: + while (true) { + if (curNode.nextSibling) { + curNode = curNode.nextSibling; + break; + } else if (curNode.parentNode !== node) { + curNode = curNode.parentNode; + } else { + break out; + } + } + } + } + + /** + * Generates the actual replaceFn which splits up text nodes + * and inserts the replacement element. + */ + function genReplacer(callback) { + function makeReplacementNode(fill, matchIndex) { + var match = matches[matchIndex]; + + if (!match.stencil) { + match.stencil = callback(match); + } + + var clone = match.stencil.cloneNode(false); + clone.setAttribute('data-mce-index', matchIndex); + + if (fill) { + clone.appendChild(dom.doc.createTextNode(fill)); + } + + return clone; + } + + return function(range) { + var before, after, parentNode, startNode = range.startNode, + endNode = range.endNode, matchIndex = range.matchIndex, + doc = dom.doc; + + if (startNode === endNode) { + var node = startNode; + + parentNode = node.parentNode; + if (range.startNodeIndex > 0) { + // Add "before" text node (before the match) + before = doc.createTextNode(node.data.substring(0, range.startNodeIndex)); + parentNode.insertBefore(before, node); + } + + // Create the replacement node: + var el = makeReplacementNode(range.match, matchIndex); + parentNode.insertBefore(el, node); + if (range.endNodeIndex < node.length) { + // Add "after" text node (after the match) + after = doc.createTextNode(node.data.substring(range.endNodeIndex)); + parentNode.insertBefore(after, node); + } + + node.parentNode.removeChild(node); + + return el; + } else { + // Replace startNode -> [innerNodes...] -> endNode (in that order) + before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex)); + after = doc.createTextNode(endNode.data.substring(range.endNodeIndex)); + var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex); + var innerEls = []; + + for (var i = 0, l = range.innerNodes.length; i < l; ++i) { + var innerNode = range.innerNodes[i]; + var innerEl = makeReplacementNode(innerNode.data, matchIndex); + innerNode.parentNode.replaceChild(innerEl, innerNode); + innerEls.push(innerEl); + } + + var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex); + + parentNode = startNode.parentNode; + parentNode.insertBefore(before, startNode); + parentNode.insertBefore(elA, startNode); + parentNode.removeChild(startNode); + + parentNode = endNode.parentNode; + parentNode.insertBefore(elB, endNode); + parentNode.insertBefore(after, endNode); + parentNode.removeChild(endNode); + + return elB; + } + }; + } + + function unwrapElement(element) { + var parentNode = element.parentNode; + parentNode.insertBefore(element.firstChild, element); + element.parentNode.removeChild(element); + } + + function getWrappersByIndex(index) { + var elements = node.getElementsByTagName('*'), wrappers = []; + + index = typeof index == "number" ? "" + index : null; + + for (var i = 0; i < elements.length; i++) { + var element = elements[i], dataIndex = element.getAttribute('data-mce-index'); + + if (dataIndex !== null && dataIndex.length) { + if (dataIndex === index || index === null) { + wrappers.push(element); + } + } + } + + return wrappers; + } + + /** + * Returns the index of a specific match object or -1 if it isn't found. + * + * @param {Match} match Text match object. + * @return {Number} Index of match or -1 if it isn't found. + */ + function indexOf(match) { + var i = matches.length; + while (i--) { + if (matches[i] === match) { + return i; + } + } + + return -1; + } + + /** + * Filters the matches. If the callback returns true it stays if not it gets removed. + * + * @param {Function} callback Callback to execute for each match. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function filter(callback) { + var filteredMatches = []; + + each(function(match, i) { + if (callback(match, i)) { + filteredMatches.push(match); + } + }); + + matches = filteredMatches; + + /*jshint validthis:true*/ + return this; + } + + /** + * Executes the specified callback for each match. + * + * @param {Function} callback Callback to execute for each match. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function each(callback) { + for (var i = 0, l = matches.length; i < l; i++) { + if (callback(matches[i], i) === false) { + break; + } + } + + /*jshint validthis:true*/ + return this; + } + + /** + * Wraps the current matches with nodes created by the specified callback. + * Multiple clones of these matches might occur on matches that are on multiple nodex. + * + * @param {Function} callback Callback to execute in order to create elements for matches. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function wrap(callback) { + if (matches.length) { + stepThroughMatches(node, matches, genReplacer(callback)); + } + + /*jshint validthis:true*/ + return this; + } + + /** + * Finds the specified regexp and adds them to the matches collection. + * + * @param {RegExp} regex Global regexp to search the current node by. + * @param {Object} [data] Optional custom data element for the match. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function find(regex, data) { + if (text && regex.global) { + while ((m = regex.exec(text))) { + matches.push(createMatch(m, data)); + } + } + + return this; + } + + /** + * Unwraps the specified match object or all matches if unspecified. + * + * @param {Object} [match] Optional match object. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function unwrap(match) { + var i, elements = getWrappersByIndex(match ? indexOf(match) : null); + + i = elements.length; + while (i--) { + unwrapElement(elements[i]); + } + + return this; + } + + /** + * Returns a match object by the specified DOM element. + * + * @param {DOMElement} element Element to return match object for. + * @return {Object} Match object for the specified element. + */ + function matchFromElement(element) { + return matches[element.getAttribute('data-mce-index')]; + } + + /** + * Returns a DOM element from the specified match element. This will be the first element if it's split + * on multiple nodes. + * + * @param {Object} match Match element to get first element of. + * @return {DOMElement} DOM element for the specified match object. + */ + function elementFromMatch(match) { + return getWrappersByIndex(indexOf(match))[0]; + } + + /** + * Adds match the specified range for example a grammar line. + * + * @param {Number} start Start offset. + * @param {Number} length Length of the text. + * @param {Object} data Custom data object for match. + * @return {DomTextMatcher} Current DomTextMatcher instance. + */ + function add(start, length, data) { + matches.push({ + start: start, + end: start + length, + text: text.substr(start, length), + data: data + }); + + return this; + } + + /** + * Returns a DOM range for the specified match. + * + * @param {Object} match Match object to get range for. + * @return {DOMRange} DOM Range for the specified match. + */ + function rangeFromMatch(match) { + var wrappers = getWrappersByIndex(indexOf(match)); + + var rng = editor.dom.createRng(); + rng.setStartBefore(wrappers[0]); + rng.setEndAfter(wrappers[wrappers.length - 1]); + + return rng; + } + + /** + * Replaces the specified match with the specified text. + * + * @param {Object} match Match object to replace. + * @param {String} text Text to replace the match with. + * @return {DOMRange} DOM range produced after the replace. + */ + function replace(match, text) { + var rng = rangeFromMatch(match); + + rng.deleteContents(); + + if (text.length > 0) { + rng.insertNode(editor.dom.doc.createTextNode(text)); + } + + return rng; + } + + /** + * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches. + * + * @return {[type]} [description] + */ + function reset() { + matches.splice(0, matches.length); + unwrap(); + + return this; + } + + text = getText(node); + + return { + text: text, + matches: matches, + each: each, + filter: filter, + reset: reset, + matchFromElement: matchFromElement, + elementFromMatch: elementFromMatch, + find: find, + add: add, + wrap: wrap, + unwrap: unwrap, + replace: replace, + rangeFromMatch: rangeFromMatch, + indexOf: indexOf + }; + }; +});