Source: ephemera.js

/**
 * ephemera.js is part of Aloha Editor project http://aloha-editor.org
 *
 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
 * Copyright (c) 2010-2014 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://aloha-editor.org/contribution.php
 *
 * TODO: currently only simple transformations are suppored, like
 *       marking classes, attributes and elements as ephemeral and removing
 *       them during the pruning process.
 *       In the future, support for the block-plugin and custom pruning
 *       functions should be added. This may be done by letting implementations
 *       completely control the pruning of a DOM element through a
 *       function that takes the content+ephemeral-data and returns only
 *       content - similar to make clean, but for single elements to reduce
 *       overhead.
 */
define([
	'strings',
	'arrays',
	'maps',
	'dom',
	'misc',
	'browsers'
], /** @exports Ephemera */ function Ephemera(
	strings,
	arrays,
	maps,
	dom,
	misc,
	Browsers
) {
	'use strict';

	var ephemeraMap = {
		classMap: {
			'aloha-ephemera-wrapper': true,
			'aloha-ephemera-filler': true,
			'aloha-ephemera-attr': true,
			'aloha-ephemera': true,
			// aloha-cleanme is the same as aloha-ephemera.
			// TODO: should be replaced with aloha-ephemera throughout the
			//       codebase and removed here.
			'aloha-cleanme': true
		},
		attrMap: {
			'hidefocus': true,
			'hideFocus': true,
			'tabindex': true,
			'tabIndex': true,
			'contenteditable': ['TABLE'],
			'contentEditable': ['TABLE']
		},
		attrRxs: [/^(?:nodeIndex|sizcache|sizset|jquery)[\w\d]*$/i],
		pruneFns: []
	};

	var commonClsSubstr = 'aloha-';

	/**
	 * Checks whether the given classes contain the substring common to all
	 * ephemeral classes. If the check fails, an warning will be logged and the
	 * substring will be set to the empty string which voids the performance
	 * improvement the common substring would otherwise have gained.
	 */
	function checkCommonSubstr(clss) {
		var i, len;
		for (i = 0, len = clss.length; i < len; i++) {
			if (-1 === clss[i].indexOf(commonClsSubstr)) {
				console.warn('Class "' + clss[i] + '" was set to be ephemeral,' + 'which hurts peformance.' + ' Add the common substring "' + commonClsSubstr + '" to the class to fix this problem.');
				commonClsSubstr = '';
			}
		}
	}

	/**
	 * Registers ephemeral classes.
	 *
	 * An ephemeral class is a non-content class that will be pruned from the
	 * from the result of editable.getContents().
	 *
	 * The given classes should contain the string 'aloha-' to get the benefit
	 * of a performance optimization.
	 *
	 * Returns a map that contains all classes that were ever registered with
	 * this function.
	 *
	 * Multiple classes may be specified. If none are specified, just returns
	 * the current ephemeral classes map without modifying it.
	 *
	 * Also see ephemera().
	 */
	function classes() {
		var clss = Array.prototype.slice.call(arguments);
		maps.fillKeys(ephemeraMap.classMap, clss, true);
		checkCommonSubstr(clss);
		/*
		Pubsub.pub('aloha.ephemera.classes', {
			ephemera: ephemeraMap,
			newClasses: clss
		});
		*/
	}

	/**
	 * Registers ephemeral attributes by attribute name.
	 *
	 * Similar to classes() except applies to entire attributes instead of
	 * individual classes in the class attribute.
	 */
	function attributes() {
		var attrs = Array.prototype.slice.call(arguments);
		maps.fillKeys(ephemeraMap.attrMap, attrs, true);
		/*
		Pubsub.pub('aloha.ephemera.attributes', {
			ephemera: ephemeraMap,
			newAttributes: attrs
		});
		*/
	}

	/**
	 * Provides access to the global ephemera registry.
	 *
	 * If the given argument is not null, sets the global ephemera registry to
	 * the given value and returns it. Otherwise, just returns the global
	 * registry.
	 *
	 * The given/returned value has the following properties:
	 *
	 * The given map may have the following entries
	 *
	 * classMap - a map from class name to the value true.
	 *            all classes must have a "aloha-" prefix.
	 *            Use Ehpemera.attributes() to set classes without "aloha-" prefix.
	 *
	 * attrMap  - a map from attribute name to the value true or to an array of
	 *            element names. If an array of elements is specified, the
	 *            attribute will only be considered ephemeral if it is found on
	 *            an element in the array.
	 *
	 * attrRxs  - an array of regexes (in object - not string - form: /[a-z].../)
	 *
	 * pruneFns - an array of functions that will be called at each pruning step.
	 *
	 * When a DOM tree is pruned with prune(elem) without an emap argument, the
	 * global registry maintained with classes() attributes() and ephemera() is
	 * used as a default map. If an emap argument is specified, the global
	 * registry will be ignored and the emap argument will be used instead.
	 *
	 * When a DOM tree is pruned with prune()
	 * - classes specified by classMap will be removed
	 * - attributes specified by attrMap or attrRxs will be removed
	 * - functions specified by pruneFns will be called as the DOM tree is
	 *   descended into (pre-order), with each node (element, text, etc.) as a
	 *   single argument. The function is free to modify the element and return
	 *   it, or return a new element which will replace the given element in the
	 *   pruned tree. If null or undefined is returned, the element will be
	 *   removed from the tree. As per contract of maps.walkDomInplace, it is
	 *   allowed to insert/remove children in the parent node as long as the
	 *   given node is not removed.
	 *
	 * Also see classes() and attributes().
	 *
	 * Note that removal of attributes doesn't always work on IE7 (in rare
	 * special cases). The dom-to-xhtml plugin can reliably remove ephemeral
	 * attributes during the serialization step.
	 */
	function ephemera(emap) {
		if (emap) {
			ephemeraMap = emap;
			/*
			Pubsub.pub('aloha.ephemera', {
				ephemera: ephemeraMap
			});
			*/
		}
		return ephemeraMap;
	}

	/**
	 * Marks an element as ephemeral.
	 *
	 * The element will be completely removed when the prune function is called
	 * on it.
	 *
	 * Adds the class 'aloha-ephemera' to the given element.
	 *
	 * The class 'aloha-ephemera' can also be added directly without recurse to
	 * this function, if that is more convenient.
	 */
	function markElement(elem) {
		dom.addClass(elem, 'aloha-ephemera');
	}

	/**
	 * Marks the attribute of an element as ephemeral.
	 *
	 * The attribute will be removed from the element when the prune function is
	 * called on it.
	 *
	 * Multiple attributes can be passed at the same time be separating them
	 * with a space.
	 *
	 * Adds the class 'aloha-ephemera-attr' to the given element. Also adds or
	 * modifies the 'data-aloha-ephemera-attr' attribute, and adds to it the
	 * name of the given attribute.
	 *
	 * These modifications can be made directly without recurse to this
	 * function, if that is more convenient.
	 */
	function markAttr(elem, attr) {
		var data = elem.getAttribute('data-aloha-ephemera-attr');
		if (null == data || '' === data) {
			data = attr;
		} else if (-1 === strings.words(data).indexOf(attr)) {
			data += ' ' + attr;
		}
		elem.setAttribute('data-aloha-ephemera-attr', data);
		dom.addClass(elem, 'aloha-ephemera-attr');
	}

	/**
	 * Marks an element as a ephemeral, excluding subnodes.
	 *
	 * The element will be removed when the prune function is called on it, but
	 * any children of the wrapper element will remain in its place.
	 *
	 * A wrapper is an element that wraps a single non-ephemeral element. A
	 * filler is an element that is wrapped by a single non-ephemeral element.
	 * This distinction is not important for the prune function, which behave
	 * the same for both wrappers and fillers, but it makes it easier to build
	 * more advanced content inspection algorithms (also see note at the header
	 * of ephemeral.js).
	 *
	 * Adds the class 'aloha-ephemera-wrapper' to the given element.
	 *
	 * The class 'aloha-ephemera-wrapper' may also be added directly, without
	 * recurse to this function, if that is more convenient.
	 *
	 * NB: a wrapper element must not wrap a filler element. Wrappers and
	 *     fillers are ephermeral. A wrapper must always wrap a single
	 *     _non-ephemeral_ element, and a filler must always fill a single
	 *     _non-ephemeral_ element.
	 */
	function markWrapper(elem) {
		dom.addClass(elem, 'aloha-ephemera-wrapper');
	}

	/**
	 * Marks an element as ephemeral, excluding subnodes.
	 *
	 * Adds the class 'aloha-ephemera-filler' to the given element.
	 *
	 * The class 'aloha-ephemera-filler' may also be added directly,
	 * without recurse to this function, if that is more convenient.
	 *
	 * See wrapper()
	 */
	function markFiller(elem) {
		dom.addClass(elem, 'aloha-ephemera-filler');
	}

	/**
	 * Prunes attributes marked as ephemeral with Ephemera.attributes() from the
	 * given element.
	 */
	function pruneMarkedAttrs(elem) {
		var data = elem.getAttribute('data-aloha-ephemera-attr');
		var i;
		var attrs;
		// Because IE7 crashes if we remove this attribute. If the dom-to-xhtml
		// plugin is turned on, it will handle the removal of this attribute
		// during serialization.
		if (!Browsers.ie7) {
			elem.removeAttribute('data-aloha-ephemera-attr');
		}
		if (typeof data === 'string') {
			attrs = strings.words(data);
			for (i = 0; i < attrs.length; i++) {
				elem.removeAttribute(attrs[i]);
			}
		}
	}

	/**
	 * Determines whether the given attribute of the given element is ephemeral
	 * according to the given emap.
	 *
	 * See Ephemera.ephemera() for an explanation of attrMap and attrRxs.
	 */
	function isAttrEphemeral(elem, attrName, attrMap, attrRxs) {
		var mapped = attrMap[attrName];
		if (mapped) {
			// The attrMap may either contain boolean true or an array of element names.
			if (true === mapped) {
				return true;
			}
			if (-1 !== mapped.indexOf(elem.nodeName)) {
				return true;
			}
		}
		return misc.anyRx(attrRxs, attrName);
	}

	/**
	 * Prunes attributes specified with either emap.attrMap or emap.attrRxs.
	 * See ephemera().
	 */
	function pruneEmapAttrs(elem, emap) {
		Maps.forEach(dom.attrs(elem), function (value, name) {
			if (isAttrEphemeral(elem, name, emap.attrMap, emap.attrRxs)) {
				elem.removeAttribute(name);
			}
		});
	}

	/**
	 * Prunes an element of attributes and classes or removes the element by
	 * returning false.
	 *
	 * Elements attributes and classes can either be marked as ephemeral, in
	 * which case the element itself will contain the prune-info, or they can be
	 * specified as ephemeral with the given emap.
	 *
	 * See ephemera() for an explanation of the emap argument.
	 */
	function pruneElem(elem, emap) {
		var className = elem.className;
		if (className && -1 !== className.indexOf(commonClsSubstr)) {
			var classes = strings.words(className);

			// Ephemera.markElement()
			if (-1 !== classes.indexOf('aloha-cleanme') || -1 !== classes.indexOf('aloha-ephemera')) {
				return false; // removes the element
			}

			// Ephemera.markWrapper() and Ephemera.markFiller()
			if (-1 !== classes.indexOf('aloha-ephemera-wrapper') || -1 !== classes.indexOf('aloha-ephemera-filler')) {
				dom.moveNextAll(elem.parentNode, elem.firstChild, elem.nextSibling);
				return false;
			}

			// Ephemera.markAttr()
			if (-1 !== classes.indexOf('aloha-ephemera-attr')) {
				pruneMarkedAttrs(elem);
			}

			// Ephemera.classes() and Ehpemera.ephemera({ classMap: {} })
			var persistentClasses = classes.filter(function (cls) {
				return !emap.classMap[cls];
			});
			if (persistentClasses.length !== classes.length) {
				if (0 === persistentClasses.length) {
					elem.removeAttribute('class');
				} else {
					elem.className = persistentClasses.join(' ');
				}
			}
		}

		// Ephemera.attributes() and Ephemera.ephemera({ attrMap: {}, attrRxs: {} })
		pruneEmapAttrs(elem, emap);

		return true;
	}

	/**
	 * Prunes the given element of all ephemeral data.
	 *
	 * Elements marked with Ephemera.markElement() will be removed.
	 * Attributes marked with Ephemera.markAttr() will be removed.
	 * Elements marked with Ephemera.markWrapper() or Ephemera.markFiller() will
	 * be replaced with their children.
	 *
	 * See ephemera() for an explanation of the emap argument.
	 *
	 * All properties of emap, if specified, are required, but may be empty.
	 *
	 * The element is modified in-place and returned.
	 */
	function prune(elem, emap) {
		emap = emap || ephemeraMap;
		// TODO
	}

	return {
		ephemera        : ephemera,
		classes         : classes,
		attributes      : attributes,
		markElement     : markElement,
		markAttr        : markAttr,
		markWrapper     : markWrapper,
		markFiller      : markFiller,
		prune           : prune,
		isAttrEphemeral : isAttrEphemeral
	};
});