'use strict';
require('../../core-upgrade.js');

var JSUtils = require('../utils/jsutils.js').JSUtils;
var wtConsts = require('../config/WikitextConstants.js');
var Consts = wtConsts.WikitextConstants;
var Util = require('../utils/Util.js').Util;
var DU = require('../utils/DOMUtils.js').DOMUtils;
var WTSUtils = require('./WTSUtils.js').WTSUtils;

/**
 * Clean up the constraints object to prevent excessively verbose output
 * and clog up log files / test runs
 */
function loggableConstraints(constraints) {
	var c = {
		a: constraints.a,
		b: constraints.b,
		min: constraints.min,
		max: constraints.max,
	};
	if (constraints.constraintInfo) {
		c.constraintInfo = {
			onSOL: constraints.constraintInfo.onSOL,
			sepType: constraints.constraintInfo.sepType,
			nodeA: constraints.constraintInfo.nodeA.nodeName,
			nodeB: constraints.constraintInfo.nodeB.nodeName,
		};
	}
	return c;
}

function precedingSeparatorTxt(n) {
	// Given the CSS white-space property and specifically,
	// "pre" and "pre-line" values for this property, it seems that any
	// sane HTML editor would have to preserve IEW in HTML documents
	// to preserve rendering. One use-case where an editor might change
	// IEW drastically would be when the user explicitly requests it
	// (Ex: pretty-printing of raw source code).
	//
	// For now, we are going to exploit this.  This information is
	// only used to extrapolate DSR values and extract a separator
	// string from source, and is only used locally.  In addition,
	// the extracted text is verified for being a valid separator.
	//
	// So, at worst, this can create a local dirty diff around separators
	// and at best, it gets us a clean diff.

	var buf = '';
	var orig = n;
	while (n) {
		if (DU.isIEW(n)) {
			buf += n.nodeValue;
		} else if (DU.isComment(n)) {
			buf += "<!--";
			buf += n.nodeValue;
			buf += "-->";
		} else if (n !== orig) { // dont return if input node!
			return null;
		}

		n = n.previousSibling;
	}

	return buf;
}

/**
 * Helper for updateSeparatorConstraints
 *
 * Collects, checks and integrates separator newline requirements to a simple
 * min, max structure.
 */
function getSepNlConstraints(state, nodeA, sepNlsHandlerA, nodeB, sepNlsHandlerB) {
	var env = state.env;
	var nlConstraints = { a: {}, b: {} };

	if (sepNlsHandlerA) {
		nlConstraints.a = sepNlsHandlerA(nodeA, nodeB, state);
		nlConstraints.min = nlConstraints.a.min;
		nlConstraints.max = nlConstraints.a.max;
	}

	if (sepNlsHandlerB) {
		nlConstraints.b = sepNlsHandlerB(nodeB, nodeA, state);
		var b = nlConstraints.b;

		// now figure out if this conflicts with the nlConstraints so far
		if (b.min !== undefined) {
			if (nlConstraints.max !== undefined && nlConstraints.max < b.min) {
				// Conflict, warn and let nodeB win.
				env.log("warning", "Incompatible constraints 1:", nodeA.nodeName,
						nodeB.nodeName, loggableConstraints(nlConstraints));
				nlConstraints.min = b.min;
				nlConstraints.max = b.min;
			} else {
				nlConstraints.min = Math.max(nlConstraints.min || 0, b.min);
			}
		}

		if (b.max !== undefined) {
			if (nlConstraints.min !== undefined && nlConstraints.min > b.max) {
				// Conflict, warn and let nodeB win.
				env.log("warning", "Incompatible constraints 2:", nodeA.nodeName,
						nodeB.nodeName, loggableConstraints(nlConstraints));
				nlConstraints.min = b.max;
				nlConstraints.max = b.max;
			} else if (nlConstraints.max !== undefined) {
				nlConstraints.max = Math.min(nlConstraints.max, b.max);
			} else {
				nlConstraints.max = b.max;
			}
		}
	}

	if (nlConstraints.max === undefined) {
		// Anything more than two lines will trigger paragraphs, so default to
		// two if nothing is specified.
		nlConstraints.max = 2;
	}

	return nlConstraints;
}

/**
 * This code assumes that the DOM is in normalized form with no run of text nodes.
 * Accumulate whitespace from the text node into state.sep.src
 *
 * returns true if the entire text node was all separator content.
 */
var handleSeparatorText = function(node) {
	var state = this.state;
	if (state.inIndentPre) {
		return false;
	}

	var text = node.nodeValue;
	var separatorREs = state.serializer.separatorREs;
	if (text.match(separatorREs.pureSepRE)) {
		state.setSep((state.sep.src || '') + text);
		return true;
	} else {
		var match = text.match(separatorREs.sepPrefixWithNlsRE);
		if (match) {
			state.setSep((state.sep.src || '') + match[0]);
		}
		return false;
	}
};

/**
 * Create a separator given a (potentially empty) separator text and newline
 * constraints
 */
function makeSeparator(state, sep, nlConstraints) {
	var origSep = sep;

	// Split on comment/ws-only lines, consuming subsequent newlines since
	// those lines are ignored by the PHP parser
	// Ignore lines with ws and a single comment in them
	var splitReString = [
		'(?:\n(?:[ \t]*?',
		Util.COMMENT_REGEXP.source,
		'[ \t]*?)+(?=\n))+|',
		Util.COMMENT_REGEXP.source,
	].join('');
	var splitRe = new RegExp(splitReString);
	var sepMatch = sep.split(splitRe).join('').match(/\n/g);
	var sepNlCount = sepMatch && sepMatch.length || 0;
	var minNls = nlConstraints.min || 0;

	if (state.atStartOfOutput && !nlConstraints.a.min && minNls > 0) {
		// Skip first newline as we are in start-of-line context
		minNls--;
	}

	if (minNls > 0 && sepNlCount < minNls) {
		// Append newlines
		var nlBuf = [];
		for (var i = 0; i < (minNls - sepNlCount); i++) {
			nlBuf.push('\n');
		}

		/* ------------------------------------------------------------------
		 * The following two heuristics try to do a best-guess on where to
		 * add the newlines relative to nodeA and nodeB that best matches
		 * wikitext output expectations.
		 *
		 * 1. In a parent-child separator scenario, where the first child of
		 *    nodeA is not an element, it could have contributed to the separator.
		 *    In that case, the newlines should be prepended because they
		 *    usually correspond to the parent's constraints,
		 *    and the separator was plucked from the child.
		 *
		 *    Try html2wt on this snippet:
		 *
		 *    a<p><!--cmt-->b</p>
		 *
		 * 2. In a sibling scenario, if nodeB is a literal-HTML element, nodeA is
		 *    forcing the newline and hence the newline should be emitted right
		 *    after it.
		 *
		 *    Try html2wt on this snippet:
		 *
		 *    <p>foo</p>  <p data-parsoid='{"stx":"html"}'>bar</p>
		 * -------------------------------------------------------------------- */
		var constraintInfo = nlConstraints.constraintInfo || {};
		var sepType = constraintInfo.sepType;
		var nodeA = constraintInfo.nodeA;
		var nodeB = constraintInfo.nodeB;
		if (sepType === 'parent-child' &&
				!DU.isContentNode(DU.firstNonDeletedChildNode(nodeA)) &&
				!(Consts.ChildTableTags.has(nodeB.nodeName) &&
					!DU.isLiteralHTMLNode(nodeB))) {
			sep = nlBuf.join('') + sep;
		} else if (sepType === 'sibling' && DU.isLiteralHTMLNode(nodeB)) {
			sep = nlBuf.join('') + sep;
		} else {
			sep = sep + nlBuf.join('');
		}
	} else if (nlConstraints.max !== undefined && sepNlCount > nlConstraints.max) {
		// Strip some newlines outside of comments
		// Capture separators in a single array with a capturing version of
		// the split regexp, so that we can work on the non-separator bits
		// when stripping newlines.
		var allBits = sep.split(new RegExp('(' + splitReString + ')'));
		var newBits = [];
		var n = sepNlCount;

		while (n > nlConstraints.max) {
			var bit = allBits.pop();
			while (bit && bit.match(splitRe)) {
				// skip comments
				newBits.push(bit);
				bit = allBits.pop();
			}
			while (n > nlConstraints.max && bit.match(/\n/)) {
				bit = bit.replace(/\n([^\n]*)/, '$1');
				n--;
			}
			newBits.push(bit);
		}
		newBits.reverse();
		newBits = allBits.concat(newBits);
		sep = newBits.join('');
	}

	state.env.log("debug/wts/sep", 'make-new   |', function() {
		var constraints = Util.clone(nlConstraints);
		constraints.constraintInfo = undefined;
		return JSON.stringify(sep) + ", " +
			JSON.stringify(origSep) + ", " +
			minNls + ", " + sepNlCount + ", " + JSON.stringify(constraints);
	});

	return sep;
}

/**
 * Merge two constraints, with the newer constraint winning in case of
 * conflicts.
 *
 * XXX: Use nesting information for conflict resolution / switch to scoped
 * constraints?
 */
function mergeConstraints(env, oldConstraints, newConstraints) {
	var res = { a: oldConstraints.a, b: newConstraints.b };
	res.min = Math.max(oldConstraints.min || 0, newConstraints.min || 0);
	res.max = Math.min(oldConstraints.max !== undefined ? oldConstraints.max : 2,
			newConstraints.max !== undefined ? newConstraints.max : 2);
	if (res.min > res.max) {
		// let newConstraints win, but complain
		if (newConstraints.max !== undefined && newConstraints.max > res.min) {
			res.max = newConstraints.max;
		} else if (newConstraints.min && newConstraints.min < res.min) {
			res.min = newConstraints.min;
		}

		res.max = res.min;
		env.log("warning", 'Incompatible constraints (merge):', res,
			loggableConstraints(oldConstraints), loggableConstraints(newConstraints));
	}
	return res;
}

/**
 * Figure out separator constraints and merge them with existing constraints
 * in state so that they can be emitted when the next content emits source.
 */
var updateSeparatorConstraints = function(nodeA, handlerA, nodeB, handlerB) {
	var nlConstraints;
	var state = this.state;
	var sepHandlerA = handlerA && handlerA.sepnls || {};
	var sepHandlerB = handlerB && handlerB.sepnls || {};
	var sepType = null;

	if (nodeA.nextSibling === nodeB) {
		// sibling separator
		sepType = "sibling";
		nlConstraints = getSepNlConstraints(state, nodeA, sepHandlerA.after,
											nodeB, sepHandlerB.before);
	} else if (nodeB.parentNode === nodeA) {
		sepType = "parent-child";
		// parent-child separator, nodeA parent of nodeB
		nlConstraints = getSepNlConstraints(state, nodeA, sepHandlerA.firstChild,
											nodeB, sepHandlerB.before);
	} else if (nodeA.parentNode === nodeB) {
		sepType = "child-parent";
		// parent-child separator, nodeB parent of nodeA
		nlConstraints = getSepNlConstraints(state, nodeA, sepHandlerA.after,
											nodeB, sepHandlerB.lastChild);
	} else {
		// sibling separator
		sepType = "sibling";
		nlConstraints = getSepNlConstraints(state, nodeA, sepHandlerA.after,
											nodeB, sepHandlerB.before);
	}

	if (nodeA.nodeName === undefined) {
		console.trace();
	}

	if (state.sep.constraints) {
		// Merge the constraints
		state.sep.constraints = mergeConstraints(this.env,
				state.sep.constraints, nlConstraints);
	} else {
		state.sep.constraints = nlConstraints;
	}

	this.env.log('debug/wts/sep', function() {
		return 'constraint' +
			' | ' + sepType +
			' | <' + nodeA.nodeName + ',' + nodeB.nodeName + '>' +
			' | ' + JSON.stringify(state.sep.constraints) +
			' | ' + DU.debugOut(nodeA) +
			' | ' + DU.debugOut(nodeB);
	});

	state.sep.constraints.constraintInfo = {
		onSOL: state.onSOL,
		// force SOL state when separator is built/emitted
		forceSOL: handlerB && handlerB.forceSOL,
		sepType: sepType,
		nodeA: nodeA,
		nodeB: nodeB,
	};
};

// spaces + (comments and anything but newline)?
var WS_COMMENTS_SEP_TEST_REGEXP = JSUtils.rejoin(
		/( +)/,
		'(', Util.COMMENT_REGEXP, /[^\n]*/, ')?$'
	);

var WS_COMMENTS_SEP_REPLACE_REGEXP = new RegExp(WS_COMMENTS_SEP_TEST_REGEXP.source, 'g');

// multiple newlines followed by spaces + (comments and anything but newline)?
var NL_WS_COMMENTS_SEP_REGEXP = JSUtils.rejoin(
		/\n+/,
		WS_COMMENTS_SEP_TEST_REGEXP
	);

function makeSepIndentPreSafe(state, sep, nlConstraints) {
	var constraintInfo = nlConstraints.constraintInfo || {};
	var sepType = constraintInfo.sepType;
	var nodeA = constraintInfo.nodeA;
	var nodeB = constraintInfo.nodeB;
	var forceSOL = constraintInfo.forceSOL && sepType !== 'child-parent';
	var origNodeB = nodeB;

	// Ex: "<div>foo</div>\n <span>bar</span>"
	//
	// We also should test for onSOL state to deal with HTML like
	// <ul> <li>foo</li></ul>
	// and strip the leading space before non-indent-pre-safe tags
	//
	// SSS FIXME: Quick hack for now. We need to probably add this
	// info to mediawiki.wikitext.constants.js or parsoid-config and
	// also see what to do about the noPre option in the wt2html pipeline
	// which has the same purpose.
	if (state.serializer.options.extName !== 'ref' && !state.inIndentPre &&
		(NL_WS_COMMENTS_SEP_REGEXP.test(sep)
		|| WS_COMMENTS_SEP_TEST_REGEXP.test(sep) && (constraintInfo.onSOL || forceSOL))
	) {
		// 'sep' is the separator before 'nodeB' and it has leading spaces on a newline.
		// We have to decide whether that leading space will trigger indent-pres in wikitext.
		// The decision depends on where this separator will be emitted relative
		// to 'nodeA' and 'nodeB'.

		var isIndentPreSafe = false;

		// Example sepType scenarios:
		//
		// 1. sibling
		//    <div>foo</div>
		//     <span>bar</span>
		//    The span will be wrapped in an indent-pre if the leading space
		//    is not stripped since span is not a block tag
		//
		// 2. child-parent
		//    <span>foo
		//     </span>bar
		//    The " </span>bar" will be wrapped in an indent-pre if the
		//    leading space is not stripped since span is not a block tag
		//
		// 3. parent-child
		//    <div>foo
		//     <span>bar</span>
		//    </div>
		//
		// In all cases, only block-tags prevent indent-pres.
		// (except for a special case for <br> nodes)
		if (nodeB && DU.precedingSpaceSuppressesIndentPre(nodeB, origNodeB)) {
			isIndentPreSafe = true;
		} else if (sepType === 'sibling' || nodeA && DU.atTheTop(nodeA)) {
			console.assert(!DU.atTheTop(nodeA) || sepType === 'parent-child');

			// 'nodeB' is the first non-separator child of 'nodeA'.
			//
			// Walk past sol-transparent nodes in the right-sibling chain
			// of 'nodeB' till we establish indent-pre safety.
			while (nodeB && (DU.isMarkerMeta(nodeB, "mw:DiffMarker") ||
					DU.emitsSolTransparentSingleLineWT(state.env, nodeB))) {
				nodeB = nodeB.nextSibling;
			}

			isIndentPreSafe = !nodeB || DU.precedingSpaceSuppressesIndentPre(nodeB, origNodeB);
		}

		// Check whether nodeB is nested inside an element that suppresses
		// indent-pres.
		//
		// 1. Walk up past zero-wikitext width nodes in the ancestor chain
		//    of 'nodeB' till we establish indent-pre safety.
		//    If nodeB uses HTML syntax, obviously it is not zero width!
		//
		// 2. Check if the ancestor is a weak/strong indent-pre suppressing tag.
		//    - Weak indent-pre suppressing tags only suppress indent-pres
		//      within immediate children.
		//    - Strong indent-pre suppressing tags suppress indent-pres
		//      in entire DOM subtree rooted at that node.

		if (nodeB && !DU.atTheTop(nodeB)) {
			var parentB = nodeB.parentNode; // could be nodeA
			while (DU.isZeroWidthWikitextElt(parentB)) {
				parentB = parentB.parentNode;
			}

			if (Consts.WeakIndentPreSuppressingTags.has(parentB.nodeName)) {
				isIndentPreSafe = true;
			} else {
				while (!DU.atTheTop(parentB)) {
					if (Consts.StrongIndentPreSuppressingTags.has(parentB.nodeName)) {
						isIndentPreSafe = true;
					}
					parentB = parentB.parentNode;
				}
			}
		}

		var stripLeadingSpace = (constraintInfo.onSOL || forceSOL) && nodeB && Consts.SolSpaceSensitiveTags.has(nodeB.nodeName);
		if (!isIndentPreSafe || stripLeadingSpace) {
			// Wrap non-nl ws from last line, but preserve comments.
			// This avoids triggering indent-pres.
			sep = sep.replace(WS_COMMENTS_SEP_REPLACE_REGEXP, function() {
				var rest = arguments[2] || '';
				if (stripLeadingSpace) {
					// No other option but to strip the leading space
					return rest;
				} else {
					// Since we nowiki-ed, we are no longer in sol state
					state.onSOL = false;
					state.hasIndentPreNowikis = true;
					return '<nowiki>' + arguments[1] + '</nowiki>' + rest;
				}
			});
		}
	}

	state.env.log("debug/wts/sep", 'ipre-safe  |', function() {
		var constraints = Util.clone(nlConstraints);
		constraints.constraintInfo = undefined;
		return JSON.stringify(sep) + ", " + JSON.stringify(constraints);
	});

	return sep;
}

/**
 * Emit a separator based on the collected (and merged) constraints
 * and existing separator text. Called when new output is triggered.
 */
var buildSep = function(node) {
	var state = this.state;
	var origNode = node;
	var src = state.env.page.src;
	var prevNode = state.sep.lastSourceNode;
	var sep, dsrA, dsrB;

	/* ----------------------------------------------------------------------
	 * Assuming we have access to the original source, we can use it only if:
	 * - If we are in rt-testing mode (NO edits in that scenario) OR
	 * - If we are in selser mode AND
	 *   . this node is not part of a subtree that has been marked 'modified'
	 *     (massively edited, either in actuality or because DOMDiff is not smart enough).
	 *   . neither node is adjacent to a deleted block node
	 *     (see the extensive comment in SSP.emitChunk in wts.SerializerState.js)
	 *
	 * In other scenarios, DSR values on "adjacent" nodes in the edited DOM
	 * may not reflect deleted content between them.
	 * ---------------------------------------------------------------------- */
	var origSepUsable = node && prevNode && node !== prevNode &&
		src && (state.rtTestMode ||
			(state.selserMode && !state.inModifiedContent &&
			!DU.nextToDeletedBlockNodeInWT(state.env, prevNode, true) &&
			!DU.nextToDeletedBlockNodeInWT(state.env, node, false) &&
			DU.origSrcValidInEditedContext(state.env, node)));
	if (origSepUsable) {
		if (!DU.isElt(prevNode)) {
			// Check if this is the last child of a zero-width element, and use
			// that for dsr purposes instead. Typical case: text in p.
			if (!prevNode.nextSibling &&
				prevNode.parentNode &&
				prevNode.parentNode !== node &&
				DU.getDataParsoid(prevNode.parentNode).dsr &&
				DU.getDataParsoid(prevNode.parentNode).dsr[3] === 0) {
				dsrA = DU.getDataParsoid(prevNode.parentNode).dsr;
			} else if (prevNode.previousSibling &&
					prevNode.previousSibling.nodeType === prevNode.ELEMENT_NODE &&
					// FIXME: Not sure why we need this check because data-parsoid
					// is loaded on all nodes. mw:Diffmarker maybe? But, if so, why?
					// Should be fixed.
					DU.getDataParsoid(prevNode.previousSibling).dsr &&
					// Don't extrapolate if the string was potentially changed
					// or we didn't diff (selser disabled)
					(state.rtTestMode || // no changes in rt testing
					// diffed and no change here
					(state.selserMode && !DU.directChildrenChanged(node.parentNode, this.env)))
			) {
				var endDsr = DU.getDataParsoid(prevNode.previousSibling).dsr[1];
				var correction;
				if (typeof (endDsr) === 'number') {
					if (DU.isComment(prevNode)) {
						correction = DU.decodedCommentLength(prevNode);
					} else {
						correction = prevNode.nodeValue.length;
					}
					dsrA = [endDsr, endDsr + correction + DU.indentPreDSRCorrection(prevNode), 0, 0];
				}
			}
		} else {
			dsrA = DU.getDataParsoid(prevNode).dsr;
		}

		if (!dsrA) {
			// nothing to do -- no reason to compute dsrB if dsrA is null
		} else if (!DU.isElt(node)) {
			// If this is the child of a zero-width element
			// and is only preceded by separator elements, we
			// can use the parent for dsr after correcting the dsr
			// with the separator run length.
			//
			// 1. text in p.
			// 2. ws-only child of a node with auto-inserted start tag
			//    Ex: "<span> <s>x</span> </s>" --> <span> <s>x</s*></span><s*> </s>
			// 3. ws-only children of a node with auto-inserted start tag
			//    Ex: "{|\n|-\n <!--foo--> \n|}"

			var npDP = DU.getDataParsoid(node.parentNode);
			if (node.parentNode !== prevNode && npDP.dsr && npDP.dsr[2] === 0) {
				var sepTxt = precedingSeparatorTxt(node);
				if (sepTxt !== null) {
					dsrB = npDP.dsr;
					if (typeof (dsrB[0]) === 'number' && sepTxt.length > 0) {
						dsrB = Util.clone(dsrB);
						dsrB[0] += sepTxt.length;
					}
				}
			}
		} else {
			if (prevNode.parentNode === node) {
				// FIXME: Maybe we shouldn't set dsr in the dsr pass if both aren't valid?
				//
				// When we are in the lastChild sep scenario and the parent doesn't have
				// useable dsr, if possible, walk up the ancestor nodes till we find
				// a dsr-bearing node
				//
				// This fix is needed to handle trailing newlines in this wikitext:
				// [[File:foo.jpg|thumb|300px|foo\n{{echo|A}}\n{{echo|B}}\n{{echo|C}}\n\n]]
				while (!node.nextSibling && !DU.atTheTop(node) &&
					(!DU.getDataParsoid(node).dsr ||
					DU.getDataParsoid(node).dsr[0] === null ||
					DU.getDataParsoid(node).dsr[1] === null)) {
					node = node.parentNode;
				}
			}

			// The top node could be a document fragment, which is not
			// an element, and so getDataParsoid will return `null`.
			dsrB = DU.isElt(node) ? DU.getDataParsoid(node).dsr : null;
		}

		// FIXME: Maybe we shouldn't set dsr in the dsr pass if both aren't valid?
		if (Util.isValidDSR(dsrA) && Util.isValidDSR(dsrB)) {
			// Figure out containment relationship
			if (dsrA[0] <= dsrB[0]) {
				if (dsrB[1] <= dsrA[1]) {
					if (dsrA[0] === dsrB[0] && dsrA[1] === dsrB[1]) {
						// Both have the same dsr range, so there can't be any
						// separators between them
						sep = '';
					} else if (dsrA[2] !== null) {
						// B in A, from parent to child
						sep = src.substring(dsrA[0] + dsrA[2], dsrB[0]);
					}
				} else if (dsrA[1] <= dsrB[0]) {
					// B following A (siblingish)
					sep = src.substring(dsrA[1], dsrB[0]);
				} else if (dsrB[3] !== null) {
					// A in B, from child to parent
					sep = src.substring(dsrA[1], dsrB[1] - dsrB[3]);
				}
			} else if (dsrA[1] <= dsrB[1]) {
				if (dsrB[3] !== null) {
					// A in B, from child to parent
					sep = src.substring(dsrA[1], dsrB[1] - dsrB[3]);
				}
			} else {
				this.env.log("warning", "dsr backwards: should not happen!");
			}

			if (state.sep.lastSourceSep) {
				sep = state.sep.lastSourceSep + sep;
			}
		}
	}

	this.env.log('debug/wts/sep', function() {
		return 'maybe-sep  | ' +
			'prev:' + (prevNode ? prevNode.nodeName : '--none--') +
			', node:' + (origNode ? origNode.nodeName : '--none--') +
			', sep: ' + JSON.stringify(sep) + ', state.sep.src: ' + JSON.stringify(state.sep.src);
	});

	// 1. Verify that the separator is really one (has to be whitespace and comments)
	// 2. If the separator is being emitted before a node that emits sol-transparent WT,
	//    go through makeSeparator to verify indent-pre constraints are met.
	var sepConstraints = state.sep.constraints || {a: {}, b: {}, max: 0};
	if (sep === undefined ||
		!WTSUtils.isValidSep(sep) ||
		(state.sep.src && state.sep.src !== sep)) {
		if (state.sep.constraints || state.sep.src) {
			// TODO: set modified flag if start or end node (but not both) are
			// modified / new so that the selser can use the separator
			sep = makeSeparator(state, state.sep.src || '', sepConstraints);
		} else {
			sep = undefined;
		}
	}

	if (sep !== undefined) {
		sep = makeSepIndentPreSafe(state, sep, sepConstraints);
	}
	return sep;
};

if (typeof module === "object") {
	module.exports.handleSeparatorText = handleSeparatorText;
	module.exports.updateSeparatorConstraints = updateSeparatorConstraints;
	module.exports.buildSep = buildSep;
}
