<commit_before>/**
 * Range.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

(function(tinymce) {
	tinymce.dom.RangeUtils = function(dom) {
		var INVISIBLE_CHAR = '\uFEFF';

		/**
		 * Walks the specified range like object and executes the callback for each sibling collection it finds.
		 *
		 * @param {Object} rng Range like object.
		 * @param {function} callback Callback function to execute for each sibling collection.
		 */
		this.walk = function(rng, callback) {
			var startContainer = rng.startContainer,
				startOffset = rng.startOffset,
				endContainer = rng.endContainer,
				endOffset = rng.endOffset,
				ancestor, startPoint,
				endPoint, node, parent, siblings, nodes;

			// Handle table cell selection the table plugin enables
			// you to fake select table cells and perform formatting actions on them
			nodes = dom.select('td.mceSelected,th.mceSelected');
			if (nodes.length > 0) {
				tinymce.each(nodes, function(node) {
					callback([node]);
				});

				return;
			}

			/**
			 * Excludes start/end text node if they are out side the range
			 *
			 * @private
			 * @param {Array} nodes Nodes to exclude items from.
			 * @return {Array} Array with nodes excluding the start/end container if needed.
			 */
			function exclude(nodes) {
				var node;

				// First node is excluded
				node = nodes[0];
				if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
					nodes.splice(0, 1);
				}

				// Last node is excluded
				node = nodes[nodes.length - 1];
				if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
					nodes.splice(nodes.length - 1, 1);
				}

				return nodes;
			};

			/**
			 * Collects siblings
			 *
			 * @private
			 * @param {Node} node Node to collect siblings from.
			 * @param {String} name Name of the sibling to check for.
			 * @return {Array} Array of collected siblings.
			 */
			function collectSiblings(node, name, end_node) {
				var siblings = [];

				for (; node && node != end_node; node = node[name])
					siblings.push(node);

				return siblings;
			};

			/**
			 * Find an end point this is the node just before the common ancestor root.
			 *
			 * @private
			 * @param {Node} node Node to start at.
			 * @param {Node} root Root/ancestor element to stop just before.
			 * @return {Node} Node just before the root element.
			 */
			function findEndPoint(node, root) {
				do {
					if (node.parentNode == root)
						return node;

					node = node.parentNode;
				} while(node);
			};

			function walkBoundary(start_node, end_node, next) {
				var siblingName = next ? 'nextSibling' : 'previousSibling';

				for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
					parent = node.parentNode;
					siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);

					if (siblings.length) {
						if (!next)
							siblings.reverse();

						callback(exclude(siblings));
					}
				}
			};

			// If index based start position then resolve it
			if (startContainer.nodeType == 1 && startContainer.hasChildNodes())
				startContainer = startContainer.childNodes[startOffset];

			// If index based end position then resolve it
			if (endContainer.nodeType == 1 && endContainer.hasChildNodes())
				endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];

			// Same container
			if (startContainer == endContainer)
				return callback(exclude([startContainer]));

			// Find common ancestor and end points
			ancestor = dom.findCommonAncestor(startContainer, endContainer);

			// Process left side
			for (node = startContainer; node; node = node.parentNode) {
				if (node === endContainer)
					return walkBoundary(startContainer, ancestor, true);

				if (node === ancestor)
					break;
			}

			// Process right side
			for (node = endContainer; node; node = node.parentNode) {
				if (node === startContainer)
					return walkBoundary(endContainer, ancestor);

				if (node === ancestor)
					break;
			}

			// Find start/end point
			startPoint = findEndPoint(startContainer, ancestor) || startContainer;
			endPoint = findEndPoint(endContainer, ancestor) || endContainer;

			// Walk left leaf
			walkBoundary(startContainer, startPoint, true);

			// Walk the middle from start to end point
			siblings = collectSiblings(
				startPoint == startContainer ? startPoint : startPoint.nextSibling,
				'nextSibling',
				endPoint == endContainer ? endPoint.nextSibling : endPoint
			);

			if (siblings.length)
				callback(exclude(siblings));

			// Walk right leaf
			walkBoundary(endContainer, endPoint);
		};

		/**
		 * Splits the specified range at it's start/end points.
		 *
		 * @param {Range/RangeObject} rng Range to split.
		 * @return {Object} Range position object.
		 */
		this.split = function(rng) {
			var startContainer = rng.startContainer,
				startOffset = rng.startOffset,
				endContainer = rng.endContainer,
				endOffset = rng.endOffset;

			function splitText(node, offset) {
				return node.splitText(offset);
			};

			// Handle single text node
			if (startContainer == endContainer && startContainer.nodeType == 3) {
				if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
					endContainer = splitText(startContainer, startOffset);
					startContainer = endContainer.previousSibling;

					if (endOffset > startOffset) {
						endOffset = endOffset - startOffset;
						startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
						endOffset = endContainer.nodeValue.length;
						startOffset = 0;
					} else {
						endOffset = 0;
					}
				}
			} else {
				// Split startContainer text node if needed
				if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
					startContainer = splitText(startContainer, startOffset);
					startOffset = 0;
				}

				// Split endContainer text node if needed
				if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
					endContainer = splitText(endContainer, endOffset).previousSibling;
					endOffset = endContainer.nodeValue.length;
				}
			}

			return {
				startContainer : startContainer,
				startOffset : startOffset,
				endContainer : endContainer,
				endOffset : endOffset
			};
		};

	};

	/**
	 * Compares two ranges and checks if they are equal.
	 *
	 * @static
	 * @param {DOMRange} rng1 First range to compare.
	 * @param {DOMRange} rng2 First range to compare.
	 * @return {Boolean} true/false if the ranges are equal.
	 */
	tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
		if (rng1 && rng2) {
			// Compare native IE ranges
			if (rng1.item || rng1.duplicate) {
				// Both are control ranges and the selected element matches
				if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))
					return true;

				// Both are text ranges and the range matches
				if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))
					return true;
			} else {
				// Compare w3c ranges
				return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
			}
		}

		return false;
	};
})(tinymce);
<commit_msg>Cleanup<commit_after>/**
 * Range.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
(function(tinymce) {
    var each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker;

    function getEndChild(container, index) {
        var childNodes = container.childNodes;

        index--;

        if (index > childNodes.length - 1) {
            index = childNodes.length - 1;
        } else if (index < 0) {
             index = 0;
        }

        return childNodes[index] || container;
    }

    tinymce.dom.RangeUtils = function(dom) {
            /**
             * Walks the specified range like object and executes the callback for each sibling collection it finds.
             *
             * @method walk
             * @param {Object} rng Range like object.
             * @param {function} callback Callback function to execute for each sibling collection.
             */
            this.walk = function(rng, callback) {
                var startContainer = rng.startContainer,
                    startOffset = rng.startOffset,
                    endContainer = rng.endContainer,
                    endOffset = rng.endOffset,
                    ancestor, startPoint,
                    endPoint, node, parent, siblings, nodes;

                // Handle table cell selection the table plugin enables
                // you to fake select table cells and perform formatting actions on them
                nodes = dom.select('td.mceSelected,th.mceSelected');
                if (nodes.length > 0) {
                    each(nodes, function(node) {
                        callback([node]);
                    });

                    return;
                }

                /**
                 * Excludes start/end text node if they are out side the range
                 *
                 * @private
                 * @param {Array} nodes Nodes to exclude items from.
                 * @return {Array} Array with nodes excluding the start/end container if needed.
                 */
                function exclude(nodes) {
                    var node;

                    // First node is excluded
                    node = nodes[0];
                    if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
                        nodes.splice(0, 1);
                    }

                    // Last node is excluded
                    node = nodes[nodes.length - 1];
                    if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
                        nodes.splice(nodes.length - 1, 1);
                    }

                    return nodes;
                }

                /**
                 * Collects siblings
                 *
                 * @private
                 * @param {Node} node Node to collect siblings from.
                 * @param {String} name Name of the sibling to check for.
                 * @return {Array} Array of collected siblings.
                 */
                function collectSiblings(node, name, end_node) {
                    var siblings = [];

                    for (; node && node != end_node; node = node[name]) {
                        siblings.push(node);
                    }

                    return siblings;
                }

                /**
                 * Find an end point this is the node just before the common ancestor root.
                 *
                 * @private
                 * @param {Node} node Node to start at.
                 * @param {Node} root Root/ancestor element to stop just before.
                 * @return {Node} Node just before the root element.
                 */
                function findEndPoint(node, root) {
                    do {
                        if (node.parentNode == root) {
                            return node;
                        }

                        node = node.parentNode;
                    } while (node);
                }

                function walkBoundary(start_node, end_node, next) {
                    var siblingName = next ? 'nextSibling' : 'previousSibling';

                    for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
                        parent = node.parentNode;
                        siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);

                        if (siblings.length) {
                            if (!next) {
                                siblings.reverse();
                            }

                            callback(exclude(siblings));
                        }
                    }
                }

                // If index based start position then resolve it
                if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
                    startContainer = startContainer.childNodes[startOffset];
                }

                // If index based end position then resolve it
                if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
                    endContainer = getEndChild(endContainer, endOffset);
                }

                // Same container
                if (startContainer == endContainer) {
                    return callback(exclude([startContainer]));
                }

                // Find common ancestor and end points
                ancestor = dom.findCommonAncestor(startContainer, endContainer);

                // Process left side
                for (node = startContainer; node; node = node.parentNode) {
                    if (node === endContainer) {
                        return walkBoundary(startContainer, ancestor, true);
                    }

                    if (node === ancestor) {
                        break;
                    }
                }

                // Process right side
                for (node = endContainer; node; node = node.parentNode) {
                    if (node === startContainer) {
                        return walkBoundary(endContainer, ancestor);
                    }

                    if (node === ancestor) {
                        break;
                    }
                }

                // Find start/end point
                startPoint = findEndPoint(startContainer, ancestor) || startContainer;
                endPoint = findEndPoint(endContainer, ancestor) || endContainer;

                // Walk left leaf
                walkBoundary(startContainer, startPoint, true);

                // Walk the middle from start to end point
                siblings = collectSiblings(
                    startPoint == startContainer ? startPoint : startPoint.nextSibling,
                    'nextSibling',
                    endPoint == endContainer ? endPoint.nextSibling : endPoint
                );

                if (siblings.length) {
                    callback(exclude(siblings));
                }

                // Walk right leaf
                walkBoundary(endContainer, endPoint);
            };

            /**
             * Splits the specified range at it's start/end points.
             *
             * @private
             * @param {Range/RangeObject} rng Range to split.
             * @return {Object} Range position object.
             */
            this.split = function(rng) {
                var startContainer = rng.startContainer,
                    startOffset = rng.startOffset,
                    endContainer = rng.endContainer,
                    endOffset = rng.endOffset;

                function splitText(node, offset) {
                    return node.splitText(offset);
                }

                // Handle single text node
                if (startContainer == endContainer && startContainer.nodeType == 3) {
                    if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
                        endContainer = splitText(startContainer, startOffset);
                        startContainer = endContainer.previousSibling;

                        if (endOffset > startOffset) {
                            endOffset = endOffset - startOffset;
                            startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
                            endOffset = endContainer.nodeValue.length;
                            startOffset = 0;
                        } else {
                            endOffset = 0;
                        }
                    }
                } else {
                    // Split startContainer text node if needed
                    if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
                        startContainer = splitText(startContainer, startOffset);
                        startOffset = 0;
                    }

                    // Split endContainer text node if needed
                    if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
                        endContainer = splitText(endContainer, endOffset).previousSibling;
                        endOffset = endContainer.nodeValue.length;
                    }
                }

                return {
                    startContainer: startContainer,
                    startOffset: startOffset,
                    endContainer: endContainer,
                    endOffset: endOffset
                };
            };

            /**
             * Normalizes the specified range by finding the closest best suitable caret location.
             *
             * @private
             * @param {Range} rng Range to normalize.
             * @return {Boolean} True/false if the specified range was normalized or not.
             */
            this.normalize = function(rng) {
                var normalized, collapsed;

                function normalizeEndPoint(start) {
                    var container, offset, walker, body = dom.getRoot(),
                        node, nonEmptyElementsMap;
                    var directionLeft, isAfterNode;

                    function hasBrBeforeAfter(node, left) {
                        var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);

                        while ((node = walker[left ? 'prev' : 'next']())) {
                            if (node.nodeName === "BR") {
                                return true;
                            }
                        }
                    }

                    function isPrevNode(node, name) {
                        return node.previousSibling && node.previousSibling.nodeName == name;
                    }

                    // Walks the dom left/right to find a suitable text node to move the endpoint into
                    // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
                    function findTextNodeRelative(left, startNode) {
                        var walker, lastInlineElement, parentBlockContainer;

                        startNode = startNode || container;
                        parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;

                        // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
                        // This:

|
 becomes
|

                        if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
                            container = startNode.parentNode;
                            offset = dom.nodeIndex(startNode);
                            normalized = true;
                            return;
                        }

                        // Walk left until we hit a text node we can move to or a block/br/img
                        walker = new TreeWalker(startNode, parentBlockContainer);
                        while ((node = walker[left ? 'prev' : 'next']())) {
                            // Break if we hit a non content editable node
                            if (dom.getContentEditableParent(node) === "false") {
                                return;
                            }

                            // Found text node that has a length
                            if (node.nodeType === 3 && node.nodeValue.length > 0) {
                                container = node;
                                offset = left ? node.nodeValue.length : 0;
                                normalized = true;
                                return;
                            }

                            // Break if we find a block or a BR/IMG/INPUT etc
                            if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
                                return;
                            }

                            lastInlineElement = node;
                        }

                        // Only fetch the last inline element when in caret mode for now
                        if (collapsed && lastInlineElement) {
                            container = lastInlineElement;
                            normalized = true;
                            offset = 0;
                        }
                    }

                    container = rng[(start ? 'start' : 'end') + 'Container'];
                    offset = rng[(start ? 'start' : 'end') + 'Offset'];
                    isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
                    nonEmptyElementsMap = dom.schema.getNonEmptyElements();
                    directionLeft = start;

                    if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
                        directionLeft = false;
                    }

                    // If the container is a document move it to the body element
                    if (container.nodeType === 9) {
                        container = dom.getRoot();
                        offset = 0;
                    }

                    // If the container is body try move it into the closest text node or position
                    if (container === body) {
                        // If start is before/after a image, table etc
                        if (directionLeft) {
                            node = container.childNodes[offset > 0 ? offset - 1 : 0];
                            if (node) {
                                if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
                                    return;
                                }
                            }
                        }

                        // Resolve the index
                        if (container.hasChildNodes()) {
                            offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
                            container = container.childNodes[offset];
                            offset = 0;

                            // Don't walk into elements that doesn't have any child nodes like a IMG
                            if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
                                // Walk the DOM to find a text node to place the caret at or a BR
                                node = container;
                                walker = new TreeWalker(container, body);

                                do {
                                    // Found a text node use that position
                                    if (node.nodeType === 3 && node.nodeValue.length > 0) {
                                        offset = directionLeft ? 0 : node.nodeValue.length;
                                        container = node;
                                        normalized = true;
                                        break;
                                    }

                                    // Found a BR/IMG element that we can place the caret before
                                    if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
                                        offset = dom.nodeIndex(node);
                                        container = node.parentNode;

                                        // Put caret after image when moving the end point
                                        if (node.nodeName == "IMG" && !directionLeft) {
                                            offset++;
                                        }

                                        normalized = true;
                                        break;
                                    }
                                } while ((node = (directionLeft ? walker.next() : walker.prev())));
                            }
                        }
                    }

                    // Lean the caret to the left if possible
                    if (collapsed) {
                        // So this: x|x
                        // Becomes: x|x
                        // Seems that only gecko has issues with this
                        if (container.nodeType === 3 && offset === 0) {
                            findTextNodeRelative(true);
                        }

                        // Lean left into empty inline elements when the caret is before a BR
                        // So this: |

                        // Becomes: |

                        // Seems that only gecko has issues with this.
                        // Special edge case for
x|

 since we don't want
x|

                        if (container.nodeType === 1) {
                            node = container.childNodes[offset];

                            // Offset is after the containers last child
                            // then use the previous child for normalization
                            if (!node) {
                                node = container.childNodes[offset - 1];
                            }

                            if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
                                !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
                                findTextNodeRelative(true, node);
                            }
                        }
                    }

                    // Lean the start of the selection right if possible
                    // So this: x[x]
                    // Becomes: x[x]
                    if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
                        findTextNodeRelative(false);
                    }

                    // Set endpoint if it was normalized
                    if (normalized) {
                        rng['set' + (start ? 'Start' : 'End')](container, offset);
                    }
                }

                collapsed = rng.collapsed;

                normalizeEndPoint(true);

                if (!collapsed) {
                    normalizeEndPoint();
                }

                // If it was collapsed then make sure it still is
                if (normalized && collapsed) {
                    rng.collapse(true);
                }

                return normalized;
            };
        };

        /**
         * Compares two ranges and checks if they are equal.
         *
         * @static
         * @method compareRanges
         * @param {DOMRange} rng1 First range to compare.
         * @param {DOMRange} rng2 First range to compare.
         * @return {Boolean} true/false if the ranges are equal.
         */
        tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
            if (rng1 && rng2) {
                // Compare native IE ranges
                if (rng1.item || rng1.duplicate) {
                    // Both are control ranges and the selected element matches
                    if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
                        return true;
                    }

                    // Both are text ranges and the range matches
                    if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
                        return true;
                    }
                } else {
                    // Compare w3c ranges
                    return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
                }
            }

            return false;
        };

        /**
         * Gets the caret range for the given x/y location.
         *
         * @static
         * @method getCaretRangeFromPoint
         * @param {Number} x X coordinate for range
         * @param {Number} y Y coordinate for range
         * @param {Document} doc Document that x/y are relative to
         * @returns {Range} caret range
         */
        tinymce.dom.RangeUtils.getCaretRangeFromPoint = function(x, y, doc) {
            var rng, point;

            if (doc.caretPositionFromPoint) {
                point = doc.caretPositionFromPoint(x, y);
                rng = doc.createRange();
                rng.setStart(point.offsetNode, point.offset);
                rng.collapse(true);
            } else if (doc.caretRangeFromPoint) {
                rng = doc.caretRangeFromPoint(x, y);
            } else if (doc.body.createTextRange) {
                rng = doc.body.createTextRange();

                try {
                    rng.moveToPoint(x, y);
                    rng.collapse(true);
                } catch (ex) {
                    // Append to top or bottom depending on drop location
                    rng.collapse(y < doc.body.clientHeight);
                }
            }

            return rng;
        };

        tinymce.dom.RangeUtils.getNode = function(container, offset) {
            if (container.nodeType == 1 && container.hasChildNodes()) {
                if (offset >= container.childNodes.length) {
                    offset = container.childNodes.length - 1;
                }

                container = container.childNodes[offset];
            }

            return container;
        };
})(tinymce);<|endoftext|>


<commit_before>
"use strict";

function t(id) {
	var s = T[id];
	if (!s) return id;
	if (!s.replace) return s;
	for (var i = 1; i < arguments.length; i++) {
		s = s.replace(new RegExp('\\\$' + i.toString(10), 'g'), arguments[i]);
	}
	return s;
}

jQuery(function ($) {
	var $c = $('#canvas'),
		$toggleUI = $('.toggleUI'),
		$loading = $('#loading'),
		$error = $('#error'),
		$loadingText = $('#loading > p'),
		$errorText = $('#error > p'),
		wordfreq = WordFreq({worker: '../wordfreq.worker.js'}),
		theme = [
			{
				fontFamily: '"Myriad Pro", "Lucida Grande", Helvetica, "Heiti TC", "微軟正黑體", "Arial Unicode MS", "Droid Fallback Sans", sans-serif',
				wordColor: 'rgba(255,255,255,0.8)',
				backgroundColor: '#353130'
			},
			{
				fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", "Arial Unicode MS", "Droid Fallback Sans", sans-serif',
				wordColor: 'rgba(0,0,0,0.7)',
				backgroundColor: 'rgba(255, 255, 255, 1)'  //opaque white
			},
			{
				// http://ethantw.net/projects/lab/css-reset/
				fontFamily: 'Baskerville, "Times New Roman", "華康儷金黑 Std", "華康儷宋 Std",  DFLiKingHeiStd-W8, DFLiSongStd-W5, "Hiragino Mincho Pro", "LiSong Pro Light", "標楷體", serif',
				wordColor: '#fff',
				backgroundColor: '#000'
			}
		],
		list, weightFactor, gridSize, themeid = 0;

	function resetCanvasSize() {
		var width = document.body.offsetWidth,
		height = document.body.offsetHeight;

		$c.attr(
			{
				height: height,
				width: width
			}
		).css({
			width: width.toString(10) + 'px',
			height: height.toString(10) + 'px',
			top: '50%',
			left: '50%',
			marginLeft: '-' + (width/2).toString(10) + 'px',
			marginTop: '-' + (height/2).toString(10) + 'px'
		});
	}

	if (
		!$.wordCloudSupported
		|| !WordFreq.supported
		|| !$.getContentSupported
		|| !Array.prototype.forEach
		|| !Array.prototype.pop
		|| !Array.prototype.push
		|| !('onhashchange' in window)
	) {
		$('#not_support').show();
		return;
	}

	if (!$.getContent.fileSupported) {
		$('input[name="source"][value="file"]').parent('label').hide();
	}

	var changeUIState = {
		start: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$('#start').show();
			resetCanvasSize();
			$c.show().wordCloud({
				wordColor: theme[themeid].wordColor,
				backgroundColor: theme[themeid].backgroundColor,
				fontFamily: theme[themeid].fontFamily,
				wait: 50,
				wordList: [
					[t('startList_1'), t('startList_1C')],
					[t('startList_2'), t('startList_2C')],
					[t('startList_3'), t('startList_3C')],
					[t('startList_4'), t('startList_4C')]
				]//,
				//abortThreshold: 200,
				//abort: changeUIState.too_slow
			});
			return false;
		},
		source: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			changeSource.apply($('input[name=source]:checked')[0]);
			$('#source_panel').show();
			return false;
		},
		ready: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			$('#title').show();
			$c.show();
			$('#ready').show();
			return false;
		},
		draw: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			$('#title').show();
			$('#controls').show();
			$c.show();
			setTimeout(
				function () {
					$c.wordCloud({
						wordColor: theme[themeid].wordColor,
						backgroundColor: theme[themeid].backgroundColor,
						fontFamily: theme[themeid].fontFamily,
						gridSize: gridSize,
						weightFactor: weightFactor, //$c[0].offsetHeight*$c[0].offsetWidth/vol*0.5,
						wordList: list//,
						//abortThreshold: 1000,
						//abort: changeUIState.too_slow
					});
				},
				0
			);
			return false;
		},
		loading: function (text) {
			$toggleUI.hide();
			$loadingText.text(text);
			$loading.fadeIn(100);
			$('#title').show();
			return false;
		},
		error: function (text) {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$errorText.text(text);
			$error.fadeIn(100);
			return false;
		}//,
		/*too_slow: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$('#too_slow').show();
			return false;
		}*/
	};

	// handleHash

	function handleHash() {
		if (window.location.hash) {
			if (window._gaq) _gaq.push(['_trackEvent', 'Word Cloud', 'hashchange: ' + window.location.hash]);
			$c.wordCloud(); // stop current wordCloud;
			switch (window.location.hash.substr(1, 4)) {
				case 'feed':
				updateTitle('feed', window.location.hash.substr(6));
				changeUIState.loading(t('downloading'));
				$.getContent(
					window.location.hash.substr(6),
					{
						type: 'feed',
						beforeComplete: processingFeed,
						complete: handleText
					}
				);
				return;
				break;
				case 'html':
				updateTitle('html', window.location.hash.substr(6));
				changeUIState.loading(t('downloading'));
				$.getContent(
					window.location.hash.substr(6),
					{
						type: 'html',
						beforeComplete: processingHTML,
						complete: handleText
					}
				);
				return;
				break;
				case 'file':
				var f = $('#file')[0];
				if (!f.files || !f.files.length) {
					// not really getting files, show panel
					if ($.getContent.fileSupported) $('input[value=file]').attr('checked', true);
					window.location.hash = '#';
					//changeUIState.source();
				} else {
					updateTitle('file', f.files[0].name);
					changeUIState.loading(t('reading'));
					$.getContent(
						f.files,
						{
							type: 'file.text',
							beforeComplete: readingFile,
							complete: handleText,
							encoding: $('#encoding').val()
						}
					);
				}
				break;
				//default:
				//do nothing
				//break;
			}
		} else {
			changeUIState.source();
		}
	}

	window.onhashchange = handleHash;
	if (window.location.hash) handleHash(); // only load when hash exists
	else changeUIState.start(); // show start otherwise

	// UI flow buttons

	$('.start').bind(
		'click',
		function () {
			window.location.hash = '#';
			changeUIState.source();
		}
	);

	$('.ready').bind(
		'click',
		changeUIState.draw
	);

	// Help/about panel

	$('.help').bind(
		'click',
		function () {
			$('#help_panel, #help_panel .entry').show();
			return false;
		}
	);

	$('#help_panel button').bind(
		'click',
		function () {
			$('#help_panel').hide();
		}
	);

	// interaction within source panel

	var $s = $('input[name=source]');

	function changeSource() {
		var type = this.value.substr(0,4);
		$('.entry').hide();
		$(this).parent().addClass('checked').siblings().removeClass('checked');
		$('#' + type + '_entry').show();
		$('.feed_type_name').text($(this).parent('label').text());
	};

	$s.bind(
		'click',
		changeSource
	);

	$('#source_panel_form').bind(
		'submit',
		function () {
			switch ($('input[name=source]:checked').val()) {
				case 'demo':
					window.location.hash = '#' + $('input[name=demo]:checked').val();
				break;
				case 'blog:blogger':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://' + $('#username').val() + '.blogspot.com/feeds/posts/default';
				break;
				case 'blog:pixnet':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://' + $('#username').val() + '.pixnet.net/blog/feed/rss';
//						function (str) {
	//						return str.replace(/\(.+?\.\.\.\)/g, '');
	//					},
				break;
				case 'blog:wretch':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://www.wretch.cc/blog/' + $('#username').val() + '&rss20=1';
				break;
				case 'blog:plurk':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://www.plurk.com/' + $('#username').val() + '.xml';
				break;
				case 'blog:twitter':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://twitter.com/statuses/user_timeline/' + $('#username').val() + '.rss';
				break;
				case 'feed':
					if (!$('#rss').val()) return false;
					window.location.hash = '#feed:' + $('#rss').val();
				break;
				case 'file':
					if (!$('#file')[0].files.length) return false;
					window.location.hash = '#file';
				break;
				case 'wiki':
					if (!$('#wikipedia_entry').val()) return false;
					window.location.hash = '#html:' + 'http://zh.wikipedia.org/zh-tw/' + encodeURIComponent($('#wikipedia_entry').val());
				break;
				case 'html':
					if (!$('#html_url').val()) return false;
					window.location.hash = '#html:' + $('#html_url').val();
				break;
			}
			return false;
		}
	);

	// helper function

	function processURL(url) {
		if (url.indexOf('%') !== -1) {
			return encodeURIComponent(decodeURI(url));
		} else {
			return encodeURIComponent(url);
		}
	}

	function updateTitle(type, title) {
		$('#title')
		.empty()
		.append('')
		.append($('').text(t('title', title)));
	}

	function processingFeed(title) {
		if (title) updateTitle('feed', title);
		changeUIState.loading(t('processing'));
	};

	function processingHTML(title) {
		if (title) updateTitle('html', data.responseData.feed.title);
		changeUIState.loading(t('processing'));
	};

	function readingFile() {
		changeUIState.loading(t('reading'));
	};

	// handleText

	function handleText (text) {
		changeUIState.loading(t('analyzing'));
		if (!text) {
			changeUIState.error(t('errorReading'));
			return;
		}
		wordfreq.empty();
		wordfreq.processText(
			text,
			function () {
				changeUIState.ready();

				list = wordfreq.getSortedList();

				if (list.length < 5) {
					changeUIState.error(t('errorWordCount'));
					return;
				}

				weightFactor = $c[0].offsetHeight * $c[0].offsetWidth / list[0][1] / 3500;
				gridSize = 8;

				var wordLength = list.length.toString(10),
				maxCount = list[0][1].toString(10);

				$c.wordCloud({
					wordColor: theme[themeid].wordColor,
					backgroundColor: theme[themeid].backgroundColor,
					fontFamily: theme[themeid].fontFamily,
					wordList: [
						[t('readyList_1', wordLength, maxCount), t('readyList_1C')],
						[t('readyList_2', wordLength, maxCount), t('readyList_2C')],
						[t('readyList_3', wordLength, maxCount), t('readyList_3C')],
						[t('readyList_4', wordLength, maxCount), t('readyList_4C')]
					]//,
					//abortThreshold: 200,
					//abort: changeUIState.too_slow
				});
			}
		);
	};

	// panel functions

	$('.smaller').bind(
		'click',
		function () {
			weightFactor -= 0.1;
			changeUIState.draw();
			return false;
		}
	);

	$('.larger').bind(
		'click',
		function () {
			weightFactor += 0.1;
			changeUIState.draw();
			return false;
		}
	);

	$('.changetheme').bind(
		'click',
		function () {
			themeid++;
			if (themeid >= theme.length) themeid = 0;
			$(document.body).css('background-color', theme[themeid].backgroundColor);
			changeUIState.draw();
			return false;
		}
	);

	$('.thiner').bind(
		'click',
		function () {
			gridSize --;
			changeUIState.draw();
			return false;
		}
	);

	$('.thicker').bind(
		'click',
		function () {
			gridSize ++;
			changeUIState.draw();
			return false;
		}
	);

	$('.save').bind(
		'click',
		function () {
			window.open($c[0].toDataURL());
			return false;
		}
	);
});<commit_msg>Don't do rotation for buttons<commit_after>
"use strict";

function t(id) {
	var s = T[id];
	if (!s) return id;
	if (!s.replace) return s;
	for (var i = 1; i < arguments.length; i++) {
		s = s.replace(new RegExp('\\\$' + i.toString(10), 'g'), arguments[i]);
	}
	return s;
}

jQuery(function ($) {
	var $c = $('#canvas'),
		$toggleUI = $('.toggleUI'),
		$loading = $('#loading'),
		$error = $('#error'),
		$loadingText = $('#loading > p'),
		$errorText = $('#error > p'),
		wordfreq = WordFreq({worker: '../wordfreq.worker.js'}),
		theme = [
			{
				fontFamily: '"Myriad Pro", "Lucida Grande", Helvetica, "Heiti TC", "微軟正黑體", "Arial Unicode MS", "Droid Fallback Sans", sans-serif',
				wordColor: 'rgba(255,255,255,0.8)',
				backgroundColor: '#353130'
			},
			{
				fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", "Arial Unicode MS", "Droid Fallback Sans", sans-serif',
				wordColor: 'rgba(0,0,0,0.7)',
				backgroundColor: 'rgba(255, 255, 255, 1)'  //opaque white
			},
			{
				// http://ethantw.net/projects/lab/css-reset/
				fontFamily: 'Baskerville, "Times New Roman", "華康儷金黑 Std", "華康儷宋 Std",  DFLiKingHeiStd-W8, DFLiSongStd-W5, "Hiragino Mincho Pro", "LiSong Pro Light", "標楷體", serif',
				wordColor: '#fff',
				backgroundColor: '#000'
			}
		],
		list, weightFactor, gridSize, themeid = 0;

	function resetCanvasSize() {
		var width = document.body.offsetWidth,
		height = document.body.offsetHeight;

		$c.attr(
			{
				height: height,
				width: width
			}
		).css({
			width: width.toString(10) + 'px',
			height: height.toString(10) + 'px',
			top: '50%',
			left: '50%',
			marginLeft: '-' + (width/2).toString(10) + 'px',
			marginTop: '-' + (height/2).toString(10) + 'px'
		});
	}

	if (
		!$.wordCloudSupported
		|| !WordFreq.supported
		|| !$.getContentSupported
		|| !Array.prototype.forEach
		|| !Array.prototype.pop
		|| !Array.prototype.push
		|| !('onhashchange' in window)
	) {
		$('#not_support').show();
		return;
	}

	if (!$.getContent.fileSupported) {
		$('input[name="source"][value="file"]').parent('label').hide();
	}

	var changeUIState = {
		start: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$('#start').show();
			resetCanvasSize();
			$c.show().wordCloud({
				wordColor: theme[themeid].wordColor,
				backgroundColor: theme[themeid].backgroundColor,
				fontFamily: theme[themeid].fontFamily,
				rotateRatio: 0,
				wait: 50,
				wordList: [
					[t('startList_1'), t('startList_1C')],
					[t('startList_2'), t('startList_2C')],
					[t('startList_3'), t('startList_3C')],
					[t('startList_4'), t('startList_4C')]
				]//,
				//abortThreshold: 200,
				//abort: changeUIState.too_slow
			});
			return false;
		},
		source: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			changeSource.apply($('input[name=source]:checked')[0]);
			$('#source_panel').show();
			return false;
		},
		ready: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			$('#title').show();
			$c.show();
			$('#ready').show();
			return false;
		},
		draw: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			resetCanvasSize();
			$('#title').show();
			$('#controls').show();
			$c.show();
			setTimeout(
				function () {
					$c.wordCloud({
						wordColor: theme[themeid].wordColor,
						backgroundColor: theme[themeid].backgroundColor,
						fontFamily: theme[themeid].fontFamily,
						gridSize: gridSize,
						weightFactor: weightFactor, //$c[0].offsetHeight*$c[0].offsetWidth/vol*0.5,
						wordList: list//,
						//abortThreshold: 1000,
						//abort: changeUIState.too_slow
					});
				},
				0
			);
			return false;
		},
		loading: function (text) {
			$toggleUI.hide();
			$loadingText.text(text);
			$loading.fadeIn(100);
			$('#title').show();
			return false;
		},
		error: function (text) {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$errorText.text(text);
			$error.fadeIn(100);
			return false;
		}//,
		/*too_slow: function () {
			$toggleUI.hide();
			$loading.fadeOut(100);
			$('#too_slow').show();
			return false;
		}*/
	};

	// handleHash

	function handleHash() {
		if (window.location.hash) {
			if (window._gaq) _gaq.push(['_trackEvent', 'Word Cloud', 'hashchange: ' + window.location.hash]);
			$c.wordCloud(); // stop current wordCloud;
			switch (window.location.hash.substr(1, 4)) {
				case 'feed':
				updateTitle('feed', window.location.hash.substr(6));
				changeUIState.loading(t('downloading'));
				$.getContent(
					window.location.hash.substr(6),
					{
						type: 'feed',
						beforeComplete: processingFeed,
						complete: handleText
					}
				);
				return;
				break;
				case 'html':
				updateTitle('html', window.location.hash.substr(6));
				changeUIState.loading(t('downloading'));
				$.getContent(
					window.location.hash.substr(6),
					{
						type: 'html',
						beforeComplete: processingHTML,
						complete: handleText
					}
				);
				return;
				break;
				case 'file':
				var f = $('#file')[0];
				if (!f.files || !f.files.length) {
					// not really getting files, show panel
					if ($.getContent.fileSupported) $('input[value=file]').attr('checked', true);
					window.location.hash = '#';
					//changeUIState.source();
				} else {
					updateTitle('file', f.files[0].name);
					changeUIState.loading(t('reading'));
					$.getContent(
						f.files,
						{
							type: 'file.text',
							beforeComplete: readingFile,
							complete: handleText,
							encoding: $('#encoding').val()
						}
					);
				}
				break;
				//default:
				//do nothing
				//break;
			}
		} else {
			changeUIState.source();
		}
	}

	window.onhashchange = handleHash;
	if (window.location.hash) handleHash(); // only load when hash exists
	else changeUIState.start(); // show start otherwise

	// UI flow buttons

	$('.start').bind(
		'click',
		function () {
			window.location.hash = '#';
			changeUIState.source();
		}
	);

	$('.ready').bind(
		'click',
		changeUIState.draw
	);

	// Help/about panel

	$('.help').bind(
		'click',
		function () {
			$('#help_panel, #help_panel .entry').show();
			return false;
		}
	);

	$('#help_panel button').bind(
		'click',
		function () {
			$('#help_panel').hide();
		}
	);

	// interaction within source panel

	var $s = $('input[name=source]');

	function changeSource() {
		var type = this.value.substr(0,4);
		$('.entry').hide();
		$(this).parent().addClass('checked').siblings().removeClass('checked');
		$('#' + type + '_entry').show();
		$('.feed_type_name').text($(this).parent('label').text());
	};

	$s.bind(
		'click',
		changeSource
	);

	$('#source_panel_form').bind(
		'submit',
		function () {
			switch ($('input[name=source]:checked').val()) {
				case 'demo':
					window.location.hash = '#' + $('input[name=demo]:checked').val();
				break;
				case 'blog:blogger':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://' + $('#username').val() + '.blogspot.com/feeds/posts/default';
				break;
				case 'blog:pixnet':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://' + $('#username').val() + '.pixnet.net/blog/feed/rss';
//						function (str) {
	//						return str.replace(/\(.+?\.\.\.\)/g, '');
	//					},
				break;
				case 'blog:wretch':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://www.wretch.cc/blog/' + $('#username').val() + '&rss20=1';
				break;
				case 'blog:plurk':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://www.plurk.com/' + $('#username').val() + '.xml';
				break;
				case 'blog:twitter':
					if (!$('#username').val()) return false;
					window.location.hash = '#feed:' + 'http://twitter.com/statuses/user_timeline/' + $('#username').val() + '.rss';
				break;
				case 'feed':
					if (!$('#rss').val()) return false;
					window.location.hash = '#feed:' + $('#rss').val();
				break;
				case 'file':
					if (!$('#file')[0].files.length) return false;
					window.location.hash = '#file';
				break;
				case 'wiki':
					if (!$('#wikipedia_entry').val()) return false;
					window.location.hash = '#html:' + 'http://zh.wikipedia.org/zh-tw/' + encodeURIComponent($('#wikipedia_entry').val());
				break;
				case 'html':
					if (!$('#html_url').val()) return false;
					window.location.hash = '#html:' + $('#html_url').val();
				break;
			}
			return false;
		}
	);

	// helper function

	function processURL(url) {
		if (url.indexOf('%') !== -1) {
			return encodeURIComponent(decodeURI(url));
		} else {
			return encodeURIComponent(url);
		}
	}

	function updateTitle(type, title) {
		$('#title')
		.empty()
		.append('')
		.append($('').text(t('title', title)));
	}

	function processingFeed(title) {
		if (title) updateTitle('feed', title);
		changeUIState.loading(t('processing'));
	};

	function processingHTML(title) {
		if (title) updateTitle('html', data.responseData.feed.title);
		changeUIState.loading(t('processing'));
	};

	function readingFile() {
		changeUIState.loading(t('reading'));
	};

	// handleText

	function handleText (text) {
		changeUIState.loading(t('analyzing'));
		if (!text) {
			changeUIState.error(t('errorReading'));
			return;
		}
		wordfreq.empty();
		wordfreq.processText(
			text,
			function () {
				changeUIState.ready();

				list = wordfreq.getSortedList();

				if (list.length < 5) {
					changeUIState.error(t('errorWordCount'));
					return;
				}

				weightFactor = $c[0].offsetHeight * $c[0].offsetWidth / list[0][1] / 3500;
				gridSize = 8;

				var wordLength = list.length.toString(10),
				maxCount = list[0][1].toString(10);

				$c.wordCloud({
					wordColor: theme[themeid].wordColor,
					backgroundColor: theme[themeid].backgroundColor,
					fontFamily: theme[themeid].fontFamily,
					rotateRatio: 0,
					wordList: [
						[t('readyList_1', wordLength, maxCount), t('readyList_1C')],
						[t('readyList_2', wordLength, maxCount), t('readyList_2C')],
						[t('readyList_3', wordLength, maxCount), t('readyList_3C')],
						[t('readyList_4', wordLength, maxCount), t('readyList_4C')]
					]//,
					//abortThreshold: 200,
					//abort: changeUIState.too_slow
				});
			}
		);
	};

	// panel functions

	$('.smaller').bind(
		'click',
		function () {
			weightFactor -= 0.1;
			changeUIState.draw();
			return false;
		}
	);

	$('.larger').bind(
		'click',
		function () {
			weightFactor += 0.1;
			changeUIState.draw();
			return false;
		}
	);

	$('.changetheme').bind(
		'click',
		function () {
			themeid++;
			if (themeid >= theme.length) themeid = 0;
			$(document.body).css('background-color', theme[themeid].backgroundColor);
			changeUIState.draw();
			return false;
		}
	);

	$('.thiner').bind(
		'click',
		function () {
			gridSize --;
			changeUIState.draw();
			return false;
		}
	);

	$('.thicker').bind(
		'click',
		function () {
			gridSize ++;
			changeUIState.draw();
			return false;
		}
	);

	$('.save').bind(
		'click',
		function () {
			window.open($c[0].toDataURL());
			return false;
		}
	);
});<|endoftext|>


<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

var React = require('react');
var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js');
var linkifyMatrix = require('./linkify-matrix');

var sanitizeHtmlParams = {
    allowedTags: [
        'font', // custom to matrix for IRC-style font coloring
        'del', // for markdown
        // deliberately no h1/h2 to stop people shouting.
        'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
        'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
        'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
    ],
    allowedAttributes: {
        // custom ones first:
        font: [ 'color' ], // custom to matrix
        a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
        // We don't currently allow img itself by default, but this
        // would make sense if we did
        img: [ 'src' ],
    },
    // Lots of these won't come up by default because we don't allow them
    selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
    // URL schemes we permit
    allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
    allowedSchemesByTag: {},

    transformTags: { // custom to matrix
        // add blank targets to all hyperlinks except vector URLs
        'a': function(tagName, attribs) {
            var m = attribs.href ? attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN) : null;
            if (m) {
                delete attribs.target;
            }
            else {
                attribs.target = '_blank';
            }
            return { tagName: tagName, attribs : attribs };
        },
    },
};

class BaseHighlighter {
    constructor(highlightClass, highlightLink) {
        this.highlightClass = highlightClass;
        this.highlightLink = highlightLink;
    }

    /**
     * apply the highlights to a section of text
     *
     * @param {string} safeSnippet The snippet of text to apply the highlights
     *     to.
     * @param {string[]} safeHighlights A list of substrings to highlight,
     *     sorted by descending length.
     *
     * returns a list of results (strings for HtmlHighligher, react nodes for
     * TextHighlighter).
     */
    applyHighlights(safeSnippet, safeHighlights) {
        var lastOffset = 0;
        var offset;
        var nodes = [];

        var safeHighlight = safeHighlights[0];
        while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
            // handle preamble
            if (offset > lastOffset) {
                var subSnippet = safeSnippet.substring(lastOffset, offset);
                nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
            }

            // do highlight. use the original string rather than safeHighlight
            // to preserve the original casing.
            var endOffset = offset + safeHighlight.length;
            nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));

            lastOffset = endOffset;
        }

        // handle postamble
        if (lastOffset != safeSnippet.length) {
            var subSnippet = safeSnippet.substring(lastOffset, undefined);
            nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
        }
        return nodes;
    }

    _applySubHighlights(safeSnippet, safeHighlights) {
        if (safeHighlights[1]) {
            // recurse into this range to check for the next set of highlight matches
            return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
        }
        else {
            // no more highlights to be found, just return the unhighlighted string
            return [this._processSnippet(safeSnippet, false)];
        }
    }
}

class HtmlHighlighter extends BaseHighlighter {
    /* highlight the given snippet if required
     *
     * snippet: content of the span; must have been sanitised
     * highlight: true to highlight as a search match
     *
     * returns an HTML string
     */
    _processSnippet(snippet, highlight) {
        if (!highlight) {
            // nothing required here
            return snippet;
        }

        var span = ""
            + snippet + "";

        if (this.highlightLink) {
            span = ""
                +span+"";
        }
        return span;
    }
}

class TextHighlighter extends BaseHighlighter {
    constructor(highlightClass, highlightLink) {
        super(highlightClass, highlightLink);
        this._key = 0;
    }

    /* create a  node to hold the given content
     *
     * snippet: content of the span
     * highlight: true to highlight as a search match
     *
     * returns a React node
     */
    _processSnippet(snippet, highlight) {
        var key = this._key++;

        var node =

                { snippet }
            ;

        if (highlight && this.highlightLink) {
            node = {node}
        }

        return node;
    }
}

module.exports = {
    /* turn a matrix event body into html
     *
     * content: 'content' of the MatrixEvent
     *
     * highlights: optional list of words to highlight, ordered by longest word first
     *
     * opts.highlightLink: optional href to add to highlights
     */
    bodyToHtml: function(content, highlights, opts) {
        opts = opts || {};

        var isHtml = (content.format === "org.matrix.custom.html");

        var safeBody;
        if (isHtml) {
            // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
            // to highlight HTML tags themselves.  However, this does mean that we don't highlight textnodes which
            // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted
            // by an attempt to search for 'foobar'.  Then again, the search query probably wouldn't work either
            try {
                if (highlights && highlights.length > 0) {
                    var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
                    var safeHighlights = highlights.map(function(highlight) {
                        return sanitizeHtml(highlight, sanitizeHtmlParams);
                    });
                    // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
                    sanitizeHtmlParams.textFilter = function(safeText) {
                        return highlighter.applyHighlights(safeText, safeHighlights).join('');
                    };
                }
                safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
            }
            finally {
                delete sanitizeHtmlParams.textFilter;
            }
            return ;
        } else {
            safeBody = content.body;
            if (highlights && highlights.length > 0) {
                var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
                return highlighter.applyHighlights(safeBody, highlights);
            }
            else {
                return safeBody;
            }
        }
    },

    highlightDom: function(element) {
        var blocks = element.getElementsByTagName("code");
        for (var i = 0; i < blocks.length; i++) {
            highlight.highlightBlock(blocks[i]);
        }
    },

}

<commit_msg>feat: unblacklist img tags with data URIs<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

var React = require('react');
var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js');
var linkifyMatrix = require('./linkify-matrix');

var sanitizeHtmlParams = {
    allowedTags: [
        'font', // custom to matrix for IRC-style font coloring
        'del', // for markdown
        // deliberately no h1/h2 to stop people shouting.
        'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
        'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
        'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img',
    ],
    allowedAttributes: {
        // custom ones first:
        font: [ 'color' ], // custom to matrix
        a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
        // We don't currently allow img itself by default, but this
        // would make sense if we did
        img: [ 'src' ],
    },
    // Lots of these won't come up by default because we don't allow them
    selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
    // URL schemes we permit
    allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
    allowedSchemesByTag: {
        img: [ 'data' ],
    },

    transformTags: { // custom to matrix
        // add blank targets to all hyperlinks except vector URLs
        'a': function(tagName, attribs) {
            var m = attribs.href ? attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN) : null;
            if (m) {
                delete attribs.target;
            }
            else {
                attribs.target = '_blank';
            }
            return { tagName: tagName, attribs : attribs };
        },
    },
};

class BaseHighlighter {
    constructor(highlightClass, highlightLink) {
        this.highlightClass = highlightClass;
        this.highlightLink = highlightLink;
    }

    /**
     * apply the highlights to a section of text
     *
     * @param {string} safeSnippet The snippet of text to apply the highlights
     *     to.
     * @param {string[]} safeHighlights A list of substrings to highlight,
     *     sorted by descending length.
     *
     * returns a list of results (strings for HtmlHighligher, react nodes for
     * TextHighlighter).
     */
    applyHighlights(safeSnippet, safeHighlights) {
        var lastOffset = 0;
        var offset;
        var nodes = [];

        var safeHighlight = safeHighlights[0];
        while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
            // handle preamble
            if (offset > lastOffset) {
                var subSnippet = safeSnippet.substring(lastOffset, offset);
                nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
            }

            // do highlight. use the original string rather than safeHighlight
            // to preserve the original casing.
            var endOffset = offset + safeHighlight.length;
            nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));

            lastOffset = endOffset;
        }

        // handle postamble
        if (lastOffset != safeSnippet.length) {
            var subSnippet = safeSnippet.substring(lastOffset, undefined);
            nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
        }
        return nodes;
    }

    _applySubHighlights(safeSnippet, safeHighlights) {
        if (safeHighlights[1]) {
            // recurse into this range to check for the next set of highlight matches
            return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
        }
        else {
            // no more highlights to be found, just return the unhighlighted string
            return [this._processSnippet(safeSnippet, false)];
        }
    }
}

class HtmlHighlighter extends BaseHighlighter {
    /* highlight the given snippet if required
     *
     * snippet: content of the span; must have been sanitised
     * highlight: true to highlight as a search match
     *
     * returns an HTML string
     */
    _processSnippet(snippet, highlight) {
        if (!highlight) {
            // nothing required here
            return snippet;
        }

        var span = ""
            + snippet + "";

        if (this.highlightLink) {
            span = ""
                +span+"";
        }
        return span;
    }
}

class TextHighlighter extends BaseHighlighter {
    constructor(highlightClass, highlightLink) {
        super(highlightClass, highlightLink);
        this._key = 0;
    }

    /* create a  node to hold the given content
     *
     * snippet: content of the span
     * highlight: true to highlight as a search match
     *
     * returns a React node
     */
    _processSnippet(snippet, highlight) {
        var key = this._key++;

        var node =

                { snippet }
            ;

        if (highlight && this.highlightLink) {
            node = {node}
        }

        return node;
    }
}

module.exports = {
    /* turn a matrix event body into html
     *
     * content: 'content' of the MatrixEvent
     *
     * highlights: optional list of words to highlight, ordered by longest word first
     *
     * opts.highlightLink: optional href to add to highlights
     */
    bodyToHtml: function(content, highlights, opts) {
        opts = opts || {};

        var isHtml = (content.format === "org.matrix.custom.html");

        var safeBody;
        if (isHtml) {
            // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
            // to highlight HTML tags themselves.  However, this does mean that we don't highlight textnodes which
            // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted
            // by an attempt to search for 'foobar'.  Then again, the search query probably wouldn't work either
            try {
                if (highlights && highlights.length > 0) {
                    var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
                    var safeHighlights = highlights.map(function(highlight) {
                        return sanitizeHtml(highlight, sanitizeHtmlParams);
                    });
                    // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
                    sanitizeHtmlParams.textFilter = function(safeText) {
                        return highlighter.applyHighlights(safeText, safeHighlights).join('');
                    };
                }
                safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
            }
            finally {
                delete sanitizeHtmlParams.textFilter;
            }
            return ;
        } else {
            safeBody = content.body;
            if (highlights && highlights.length > 0) {
                var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
                return highlighter.applyHighlights(safeBody, highlights);
            }
            else {
                return safeBody;
            }
        }
    },

    highlightDom: function(element) {
        var blocks = element.getElementsByTagName("code");
        for (var i = 0; i < blocks.length; i++) {
            highlight.highlightBlock(blocks[i]);
        }
    },

}

<|endoftext|>


<commit_before>function ImageInfo(url, params) {
    var self = this;

    self.image = new Image();

    self.Init = function () {
        self.center = params && params.center ? params.center : { x: self.image.width / 2, y: self.image.height / 2 };
        self.size = params && params.size ? params.size : { width: self.image.width, height: self.image.height};
        self.radius = Math.sqrt(Math.pow(self.size.width, 2.0) + Math.pow(self.size.height, 2.0));

        self.lifespan = params && params.lifespan ? params.lifespan : 100000000;
        self.animated = params && params.animated && params.animated == true;
    };

    self.image.onload = self.Init;
    self.image.src = url;
}<commit_msg>Fixed radius calculation<commit_after>function ImageInfo(url, params) {
    var self = this;

    self.image = new Image();

    self.Init = function () {
        self.center = params && params.center ? params.center : { x: self.image.width / 2, y: self.image.height / 2 };
        self.size = params && params.size ? params.size : { width: self.image.width, height: self.image.height};
        self.radius = Math.sqrt(Math.pow(self.size.width, 2.0) + Math.pow(self.size.height, 2.0)) / 2.0;

        self.lifespan = params && params.lifespan ? params.lifespan : 100000000;
        self.animated = params && params.animated && params.animated == true;
    };

    self.image.onload = self.Init;
    self.image.src = url;
}<|endoftext|>


<commit_before>var GOVUK = {};

/*
  General utility methods
  ======================= */
GOVUK.utils = (new function() {

  /* Parse the URL to retrieve a value.
   * @name (String) Name of URL param
   * e.g.
   * GOVUK.utils.getParameterByName('a_param');
   **/
  this.getParameterByName = function(name) {
    var param = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    var qs = document.location.search.replace("&", "&");
    var regex = new RegExp("[\\?&]" + param + "=([^&#]*)");
    var results = regex.exec(qs);
    return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
  }
});

/*
  Cookie methods
  ==============
  Setting a cookie:
  GOVUK.cookie.set('hobnob', 'tasty', { days: 30 });

  Reading a cookie:
  GOVUK.cookie.get('hobnob');

  Deleting a cookie:
  GOVUK.cookie.set('hobnob', null);
*/
GOVUK.cookie = (new function() {

  /* Set a cookie.
   * @name (String) Name of cookie
   * @value (String) Value to store
   * @options (Object) Optional configurations
   **/
  this.set = function(name, value, options) {
    var opts = options || {};
    var str = name + "=" + value + "; path=/";
    var domain, domainSplit;
    if (opts.days) {
      var date = new Date();
      date.setTime(date.getTime() + (opts.days * 24 * 60 * 60 * 1000));
      str += "; expires=" + date.toGMTString();
    }

    if(opts.domain) {
      str += "; domain=" + opts.domain;
    }

    if (document.location.protocol == 'https:'){
      str += "; Secure";
    }

    document.cookie = str;
  }

 /* Read a cookie
  * @name (String) Name of cookie to read.
  **/
  this.get = function(name) {
    var nameEQ = name + "=";
    var cookies = document.cookie.split(';');
    var value;

    for(var i = 0, len = cookies.length; i < len; i++) {
      var cookie = cookies[i];
      while (cookie.charAt(0) == ' ') {
        cookie = cookie.substring(1, cookie.length);
      }
      if (cookie.indexOf(nameEQ) === 0) {
        value = decodeURIComponent(cookie.substring(nameEQ.length));
      }
    }
    return value;
  }

  /* Delete a cookie.
   * @name (String) Name of cookie
   **/
  this.remove = function(name) {
    this.set(name, null);
  }

});

/*
  UTM value storage
  =================
  Store values from URL param:
  GOVUK.utm.set();

  Reading stored values:
  GOVUK.utm.get();
*/
GOVUK.utm = (new function() {
  var utils = GOVUK.utils;

  this.set = function() {
    // params = [utm_campaign|utm_content|utm_medium|utm_source\utm_term]
    var params = document.location.search.match(/utm_[a-z]+/g) || [];
    var domain = document.getElementById("utmCookieDomain");
    var config = { days: 7 };
    var data = {};
    var json, value;

    if(domain) {
      config.domain = domain.getAttribute("value");
    }

    // 1. Does not add empty values.
    for(var i=0; i<params.length; ++i) {
      value = utils.getParameterByName(params[i]);
      if(value) {
        data[params[i]] = value;
      }
    }

    json = JSON.stringify(data);
    if(json.length > 2) { // ie. not empty
      GOVUK.cookie.set("ed_utm", json, config);
    }
  }

  this.get = function() {
    var cookie = GOVUK.cookie.get("ed_utm");
    return cookie ? JSON.parse(cookie) : null;
  }

});

GOVUK.components = (new function() {

  /* Adds a label element to mirror the matched selected option
   * text of a <select> input, for enhanced display purpose.
   * @$select (jQuery node) Target input element
   **/
  this.SelectTracker = SelectTracker;
  function SelectTracker($select) {
    var SELECT_TRACKER = this;
    var button, code, lang;

    if(arguments.length && $select.length) {
      this.$node = $(document.createElement("p"));
      this.$node.attr("aria-hidden", "true");
      this.$node.addClass("SelectTracker");
      this.$select = $select;
      this.$select.addClass("trackedSelect");
      this.$select.before(this.$node);
      this.$select.on("change.SelectTracker", function() {
        SELECT_TRACKER.update();
      });

      // Initial value
      this.update();
    }
  }
  SelectTracker.prototype = {};
  SelectTracker.prototype.update = function() {
    this.$node.text(this.$select.find(":selected").text());
  }

  /* Extends SelectTracker to meet additional display requirement
   * @$select (jQuery node) Target input element
   **/
  this.LanguageSelectTracker = LanguageSelectTracker;
  function LanguageSelectTracker($select) {
    SelectTracker.call(this, $select);
    if(this.$node) {
      this.$node.addClass("LanguageSelectTracker");
    }
  }
  LanguageSelectTracker.prototype = new SelectTracker;
  LanguageSelectTracker.prototype.update = function() {
    var $code = $(document.createElement("span"));
    var $lang = $(document.createElement("span"));
    SelectTracker.prototype.update.call(this);
    $lang.addClass("lang");
    $code.addClass("code");
    $lang.text(this.$node.text());
    $code.text(this.$select.val());
    this.$node.empty();
    this.$node.append($code);
    this.$node.append($lang);
  }

});

/* In test mode we don't want the code to
 * run immediately because we have to compensate
 * for not having a browser environment first.
 **/
GOVUK.page = (new function() {

  // What to run on every page (called from <body>).
  this.init = function() {
    captureUtmValue();
    enhanceLanguageSwitcher();
  }

  /* Attempt to capture UTM information if we haven't already
   * got something and querystring is not empty.
   **/
  function captureUtmValue() {
    var captured = GOVUK.utm.get();
    if(!captured && document.location.search.substring(1)) {
      GOVUK.utm.set();
    }
  }

  /* Adapt the language switcher <select> to enable
   * a more aesthetically pleasing view.
   **/
  function enhanceLanguageSwitcher() {
    var $select = $("#id_lang");
    if($select.length) {
      new GOVUK.components.LanguageSelectTracker($select);
    }
  }
});
<commit_msg>Add auto-submit to compensate for lack of button on new language switcher control<commit_after>var GOVUK = {};

/*
  General utility methods
  ======================= */
GOVUK.utils = (new function() {

  /* Parse the URL to retrieve a value.
   * @name (String) Name of URL param
   * e.g.
   * GOVUK.utils.getParameterByName('a_param');
   **/
  this.getParameterByName = function(name) {
    var param = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    var qs = document.location.search.replace("&", "&");
    var regex = new RegExp("[\\?&]" + param + "=([^&#]*)");
    var results = regex.exec(qs);
    return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
  }
});

/*
  Cookie methods
  ==============
  Setting a cookie:
  GOVUK.cookie.set('hobnob', 'tasty', { days: 30 });

  Reading a cookie:
  GOVUK.cookie.get('hobnob');

  Deleting a cookie:
  GOVUK.cookie.set('hobnob', null);
*/
GOVUK.cookie = (new function() {

  /* Set a cookie.
   * @name (String) Name of cookie
   * @value (String) Value to store
   * @options (Object) Optional configurations
   **/
  this.set = function(name, value, options) {
    var opts = options || {};
    var str = name + "=" + value + "; path=/";
    var domain, domainSplit;
    if (opts.days) {
      var date = new Date();
      date.setTime(date.getTime() + (opts.days * 24 * 60 * 60 * 1000));
      str += "; expires=" + date.toGMTString();
    }

    if(opts.domain) {
      str += "; domain=" + opts.domain;
    }

    if (document.location.protocol == 'https:'){
      str += "; Secure";
    }

    document.cookie = str;
  }

 /* Read a cookie
  * @name (String) Name of cookie to read.
  **/
  this.get = function(name) {
    var nameEQ = name + "=";
    var cookies = document.cookie.split(';');
    var value;

    for(var i = 0, len = cookies.length; i < len; i++) {
      var cookie = cookies[i];
      while (cookie.charAt(0) == ' ') {
        cookie = cookie.substring(1, cookie.length);
      }
      if (cookie.indexOf(nameEQ) === 0) {
        value = decodeURIComponent(cookie.substring(nameEQ.length));
      }
    }
    return value;
  }

  /* Delete a cookie.
   * @name (String) Name of cookie
   **/
  this.remove = function(name) {
    this.set(name, null);
  }

});

/*
  UTM value storage
  =================
  Store values from URL param:
  GOVUK.utm.set();

  Reading stored values:
  GOVUK.utm.get();
*/
GOVUK.utm = (new function() {
  var utils = GOVUK.utils;

  this.set = function() {
    // params = [utm_campaign|utm_content|utm_medium|utm_source\utm_term]
    var params = document.location.search.match(/utm_[a-z]+/g) || [];
    var domain = document.getElementById("utmCookieDomain");
    var config = { days: 7 };
    var data = {};
    var json, value;

    if(domain) {
      config.domain = domain.getAttribute("value");
    }

    // 1. Does not add empty values.
    for(var i=0; i<params.length; ++i) {
      value = utils.getParameterByName(params[i]);
      if(value) {
        data[params[i]] = value;
      }
    }

    json = JSON.stringify(data);
    if(json.length > 2) { // ie. not empty
      GOVUK.cookie.set("ed_utm", json, config);
    }
  }

  this.get = function() {
    var cookie = GOVUK.cookie.get("ed_utm");
    return cookie ? JSON.parse(cookie) : null;
  }

});

GOVUK.components = (new function() {

  /* Adds a label element to mirror the matched selected option
   * text of a <select> input, for enhanced display purpose.
   * @$select (jQuery node) Target input element
   **/
  this.SelectTracker = SelectTracker;
  function SelectTracker($select) {
    var SELECT_TRACKER = this;
    var button, code, lang;

    if(arguments.length && $select.length) {
      this.$node = $(document.createElement("p"));
      this.$node.attr("aria-hidden", "true");
      this.$node.addClass("SelectTracker");
      this.$select = $select;
      this.$select.addClass("trackedSelect");
      this.$select.before(this.$node);
      this.$select.on("change.SelectTracker", function() {
        SELECT_TRACKER.update();
      });

      // Initial value
      this.update();
    }
  }
  SelectTracker.prototype = {};
  SelectTracker.prototype.update = function() {
    this.$node.text(this.$select.find(":selected").text());
  }

  /* Extends SelectTracker to meet additional display requirement
   * @$select (jQuery node) Target input element
   **/
  this.LanguageSelectTracker = LanguageSelectTracker;
  function LanguageSelectTracker($select) {
    SelectTracker.call(this, $select);
    if(this.$node) {
      this.$node.addClass("LanguageSelectTracker");
    }
  }
  LanguageSelectTracker.prototype = new SelectTracker;
  LanguageSelectTracker.prototype.update = function() {
    var $code = $(document.createElement("span"));
    var $lang = $(document.createElement("span"));
    SelectTracker.prototype.update.call(this);
    $lang.addClass("lang");
    $code.addClass("code");
    $lang.text(this.$node.text());
    $code.text(this.$select.val());
    this.$node.empty();
    this.$node.append($code);
    this.$node.append($lang);
  }

});

/* In test mode we don't want the code to
 * run immediately because we have to compensate
 * for not having a browser environment first.
 **/
GOVUK.page = (new function() {

  // What to run on every page (called from <body>).
  this.init = function() {
    captureUtmValue();
    enhanceLanguageSwitcher();
  }

  /* Attempt to capture UTM information if we haven't already
   * got something and querystring is not empty.
   **/
  function captureUtmValue() {
    var captured = GOVUK.utm.get();
    if(!captured && document.location.search.substring(1)) {
      GOVUK.utm.set();
    }
  }

  /* Adapt the language switcher <select> to enable
   * a more aesthetically pleasing view.
   **/
  function enhanceLanguageSwitcher() {
    var $select = $("#id_lang");
    if($select.length) {
      new GOVUK.components.LanguageSelectTracker($select);
      $select.on("change", function() {
        this.form.submit();
      })
    }
  }
});
<|endoftext|>