We use cookies to make our website more effective. By using our website you agree to our privacy policy.

Source: html/elements.js

/**
 * html/elements.js is part of Aloha Editor project http://www.alohaeditor.org
 *
 * Aloha Editor ● JavaScript Content Editing Library
 * Copyright (c) 2010-2015 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://www.alohaeditor.org/docs/contributing.html
 */
define([
	'html/styles',
	'html/predicates',
	'dom',
	'arrays',
	'cursors',
	'strings'
], function (
	Styles,
	Predicates,
	Dom,
	Arrays,
	Cursors,
	Strings
) {
	'use strict';

	/**
	 * Checks whether the given node should be treated like a void element.
	 *
	 * Void elements like IMG and INPUT are considered as void type, but so are
	 * "block" (elements inside of editale regions that are not themselves
	 * editable).
	 *
	 * @param  {Node} node
	 * @return {boolean}
	 * @memberOf html
	 */
	function isVoidType(node) {
		return Predicates.isVoidNode(node) || !Dom.isEditableNode(node);
	}

	/**
	 * Returns true if the given node is unrendered whitespace, with the caveat
	 * that it only examines the given node and not any siblings.  An additional
	 * check is necessary to determine whether the node occurs after/before a
	 * linebreaking node.
	 *
	 * Taken from
	 * http://code.google.com/p/rangy/source/browse/trunk/src/js/modules/rangy-cssclassapplier.js
	 * under the MIT license.
	 *
	 * @private
	 * @param  {Node} node
	 * @return {boolean}
	 */
	function isUnrenderedWhitespaceNoBlockCheck(node) {
		if (!Dom.isTextNode(node)) {
			return false;
		}
		if (!node.length) {
			return true;
		}
		if (Strings.NOT_SPACE.test(node.nodeValue)
				|| Strings.NON_BREAKING_SPACE.test(node.nodeValue)) {
			return false;
		}
		var cssWhiteSpace;
		if (node.parentNode) {
			cssWhiteSpace = Dom.getComputedStyle(node.parentNode, 'white-space');
			if (Styles.isWhiteSpacePreserveStyle(cssWhiteSpace)) {
				return false;
			}
		}
		if ('pre-line' === cssWhiteSpace) {
            if (/[\r\n]/.test(node.data)) {
                return false;
            }
        }
		return true;
	}

	/**
	 * Tags representing non-block-level elements which are nevertheless line
	 * breaking.
	 *
	 * @private
	 * @type {Object.<string, boolean>}
	 */
	var LINE_BREAKING_VOID_ELEMENTS = {
		'BR'  : true,
		'HR'  : true,
		'IMG' : true
	};

	/**
	 * Returns true if the node at point is unrendered, with the caveat that it
	 * only examines the node at point and not any siblings.  An additional
	 * check is necessary to determine whether the whitespace occurrs
	 * after/before a linebreaking node.
	 *
	 * @private
	 */
	function isUnrenderedAtPoint(point) {
		return (isUnrenderedWhitespaceNoBlockCheck(point.node)
				|| (Dom.isElementNode(point.node)
					&& Styles.hasInlineStyle(point.node)
					&& !LINE_BREAKING_VOID_ELEMENTS[point.node]));
	}

	/**
	 * Tries to move the given point to the end of the line, stopping to the
	 * left of a br or block node, ignoring any unrendered nodes. Returns true
	 * if the point was successfully moved to the end of the line, false if some
	 * rendered content was encountered on the way. point will not be mutated
	 * unless true is returned.
	 *
	 * @private
	 * @param  {Cursor} point
	 * @return {boolean} True if the cursor is moved
	 */
	function skipUnrenderedToEndOfLine(point) {
		var cursor = point.clone();
		cursor.nextWhile(isUnrenderedAtPoint);
		if (!Styles.hasLinebreakingStyle(cursor.node)) {
			return false;
		}
		point.setFrom(cursor);
		return true;
	}

	/**
	 * Tries to move the given point to the start of the line, stopping to the
	 * right of a br or block node, ignoring any unrendered nodes. Returns true
	 * if the point was successfully moved to the start of the line, false if
	 * some rendered content was encountered on the way. point will not be
	 * mutated unless true is returned.
	 *
	 * @private
	 * @param {Cursor} point
	 * @return {boolean} True if the cursor is moved
	 */
	function skipUnrenderedToStartOfLine(point) {
		var cursor = point.clone();
		cursor.prev();
		cursor.prevWhile(isUnrenderedAtPoint);
		if (!Styles.hasLinebreakingStyle(cursor.node)) {
			return false;
		}
		var isBr = ('BR' === cursor.node.nodeName);
		cursor.next(); // after/out of the linebreaking node
		// Because point may be to the right of a br at the end of a
		// block, in which case the line starts before the br.
		if (isBr) {
			var endOfBlock = point.clone();
			if (skipUnrenderedToEndOfLine(endOfBlock) && endOfBlock.atEnd) {
				cursor.skipPrev(); // before the br
				cursor.prevWhile(isUnrenderedAtPoint);
				if (!Styles.hasLinebreakingStyle(cursor.node)) {
					return false;
				}
				cursor.next(); // after/out of the linebreaking node
			}
		}
		point.setFrom(cursor);
		return true;
	}

	/**
	 * Returns true if the given node is unrendered whitespace.
	 *
	 * @param  {Node} node
	 * @return {boolean}
	 */
	function isUnrenderedWhitespace(node) {
		if (!isUnrenderedWhitespaceNoBlockCheck(node)) {
			return false;
		}
		return skipUnrenderedToEndOfLine(Cursors.cursor(node, false))
		    || skipUnrenderedToStartOfLine(Cursors.cursor(node, false));
	}

	/**
	 * Returns true if node is either the first or last child of its parent.
	 *
	 * @private
	 * @param  {Node} node
	 * @return {boolean}
	 */
	function isTerminalNode(node) {
		var parent = node.parentNode;
		return parent
		    && (node === parent.firstChild || node === parent.lastChild);
	}

	/**
	 * Checks whether the given node is next to a block level element.
	 *
	 * @private
	 * @param  {Node} node
	 * @return {boolean}
	 */
	function isAdjacentToBlock(node) {
		return (node.previousSibling && Predicates.isBlockNode(node.previousSibling))
		    || (node.nextSibling && Predicates.isBlockNode(node.nextSibling));
	}

	/**
	 * Checks whether the given node is visually rendered according to HTML5
	 * specification.
	 *
	 * @param  {Node} node
	 * @return {boolean}
	 * @memberOf html
	 */
	function isUnrendered(node) {
		if (!Predicates.isVoidNode(node)
				// Because empty list elements are rendered
				&& !Predicates.isListItem(node)
				&& 0 === Dom.nodeLength(node)) {
			return true;
		}

		if (node.firstChild && !Dom.nextWhile(node.firstChild, isUnrendered)) {
			return true;
		}

		// Because isUnrenderedWhiteSpaceNoBlockCheck() will give us false
		// positives but never false negatives, the algorithm that will follow
		// will make certain, and will also consider unrendered <br>s
		var maybeUnrenderedNode = isUnrenderedWhitespaceNoBlockCheck(node);

		// Because a <br> element that is a child node adjacent to its parent's
		// end tag (terminal sibling) must not be rendered
		if (!maybeUnrenderedNode) {
			if ('BR' === node.nodeName
				&& isTerminalNode(node)
				&& Styles.hasLinebreakingStyle(node.parentNode)) {
				if (node.nextSibling && 'BR' === node.nextSibling.nodeName) {
					return true;
				}
				if (node.previousSibling && 'BR' === node.previousSibling.nodeName) {
					return true;
				}
				if (node.nextSibling && Dom.nextWhile(node.nextSibling, isUnrendered)) {
					return true;
				}
				if (node.previousSibling && Dom.prevWhile(node.previousSibling, isUnrendered)) {
					return true;
				}
			}
			return false;
		}

		if (isTerminalNode(node)) {
			if (!Dom.isTextNode(node)) {
				return false;
			}
			var inlineNode = Dom.nextNonAncestor(node, false, function (node) {
				return Predicates.isInlineNode(node) && !isUnrendered(node);
			}, function (node) {
				return Styles.hasLinebreakingStyle(node) || Dom.isEditingHost(node);
			});
			return !inlineNode;
		}

		return isAdjacentToBlock(node)
		    || skipUnrenderedToEndOfLine(Cursors.create(node, false))
		    || skipUnrenderedToStartOfLine(Cursors.create(node, false));
	}

	/**
	 * Returns true of the given node is rendered.
	 *
	 * @param  {Node} node
	 * @return {boolean}
	 * @memberOf html
	 */
	function isRendered(node) {
		return !isUnrendered(node);
	}

	/**
	 * Parses the given markup string and returns an array of detached top-level
	 * elements. Event handler attributes will not trigger immediately to prevent
	 * XSS, so aloha.editor.parse('<img src="" onerror="alert(1)">', document);
	 * will NOT generate an alert box. See https://github.com/alohaeditor/Aloha-Editor/issues/1270
	 * for details.
	 *
	 * @param  {string}   html
	 * @param  {Document} doc
	 * @return {Array.<Node>}
	 * @memberOf html
	 */
	function parse(html, doc) {
		var context = (doc.implementation && doc.implementation.createHTMLDocument)
			? doc.implementation.createHTMLDocument()
			: doc;
		var parser = context.createElement('DIV');
		parser.innerHTML = html;
		var nodes = Arrays.coerce(parser.childNodes);
		nodes.forEach(Dom.remove);
		return nodes;
	}

	/**
	 * Checks whether the given node is a BR element that is placed within an
	 * otherwise empty line-breaking element to ensure that the line-breaking
	 * element it will be rendered (with one line-height).
	 *
	 * @param  {Node} node
	 * @return {boolean}
	 */
	function isProppingBr(node) {
		var parent = node.parentNode;
		if ('BR' !== node.nodeName || !parent) {
			return false;
		}
		if (!Styles.hasLinebreakingStyle(parent)) {
			return false;
		}
		var rendered = Dom.children(parent).filter(isRendered);
		return 1 === rendered.length && node === rendered[0];
	}

	return {
		parse                              : parse,
		isVoidType                         : isVoidType,
		isRendered                         : isRendered,
		isUnrendered                       : isUnrendered,
		isUnrenderedWhitespace             : isUnrenderedWhitespace,
		isUnrenderedWhitespaceNoBlockCheck : isUnrenderedWhitespaceNoBlockCheck,
		isProppingBr                       : isProppingBr,
		skipUnrenderedToEndOfLine          : skipUnrenderedToEndOfLine,
		skipUnrenderedToStartOfLine        : skipUnrenderedToStartOfLine
	};
});
comments powered by Disqus