'use strict';

import { containsPos, rangeFromNode, narrowToNonSpace, comparePos } from '../utils';
import createConfig from '../config';
import { Abbreviation } from '../abbreviation';

/**
 * Marks selected text or matched node content with abbreviation
 * @param {CodeMirror} editor
 */
export default function wrapWithAbbreviation(editor) {
	const range = getWrappingContentRange(editor);

	if (range) {
		const prompt = editor.getOption('emmetPrompt') || defaultPrompt;
		const text = editor.getRange(range.from, range.to, '\n')
			.split('\n')
			.map(line => line.trim());

		prompt(editor, 'Enter abbreviation to wrap with:', abbr => {
			if (abbr) {
				const model = new Abbreviation(abbr, range, createConfig(editor, range.from, { text }));
				model.insert(editor);
			}
		});
	} else {
		console.warn('Nothing to wrap');
	}
}

/**
 * Returns content range that should be wrapped
 * @param {CodeMirror} editor
 */
function getWrappingContentRange(editor) {
	if (editor.somethingSelected()) {
		const sel = editor.listSelections().filter(sel => sel.anchor !== sel.head)[0];
		if (sel) {
			return  comparePos(sel.anchor, sel.head) < 0
				? { from: sel.anchor, to: sel.head }
				: { from: sel.head, to: sel.anchor };
		}
	}

	// Nothing selected, find parent HTML node and return range for its content
	return getTagRangeForPos(editor, editor.getCursor());
}

/**
 * Returns either inner or outer tag range (depending on `pos` location)
 * for given position
 * @param {CodeMirror} editor
 * @param {Object} pos
 * @return {Object}
 */
function getTagRangeForPos(editor, pos) {
	const model = editor.getEmmetDocumentModel();
	const tag = model && model.nodeForPoint(pos);

	if (!tag) {
		return null;
	}

	// Depending on given position, return either outer or inner tag range
	if (inRange(tag.open, pos) || inRange(tag.close, pos)) {
		// Outer range
		return rangeFromNode(tag);
	}

	// Inner range
	const from = tag.open.end;
	const to = tag.close ? tag.close.start : tag.open.end;

	return narrowToNonSpace(editor, from, to);
}

function inRange(tag, pos) {
	return tag && containsPos(rangeFromNode(tag), pos);
}

function defaultPrompt(editor, message, callback) {
	callback(window.prompt(message));
}


'use strict';

export default function(editor) {
	const between = editor.listSelections().map(sel => betweenTags(editor, sel));

	if (!between.some(Boolean)) {
		return editor.constructor.Pass;
	}

	editor.operation(() => {
		let sels = editor.listSelections();
		const singleSep = editor.doc.lineSeparator();
		const doubleSep = singleSep + singleSep;

		// Step 1: insert newlines either single or double depending on selection
		for (let i = sels.length - 1; i >= 0; i--) {
			editor.replaceRange(between[i] ? doubleSep : singleSep, sels[i].anchor, sels[i].head, '+newline');
		}

		// Step 2: indent inserted lines
		sels = editor.listSelections();
		for (let i = 0; i < sels.length; i++) {
			editor.indentLine(sels[i].from().line, null, true);

			if (between[i]) {
				editor.indentLine(sels[i].from().line - 1, null, true);
			}
		}

		// Step 3: adjust caret positions
		editor.setSelections(editor.listSelections().map((sel, i) => {
			if (between[i]) {
				const line = sel.from().line - 1;
				const cursor = {
					line,
					ch: editor.getLine(line).length
				};
				return { anchor: cursor, head: cursor };
			}

			return sel;
		}));
	});
}

/**
 * Check if given range is a single caret between tags
 * @param {CodeMirror} editor
 * @param {CodeMirror.range} range
 */
function betweenTags(editor, range) {
	if (equalCursorPos(range.anchor, range.head)) {
		const cursor = range.anchor;
		const mode = editor.getModeAt(cursor);

		if (mode.name === 'xml') {
			const left = editor.getTokenAt(cursor);
			const right = editor.getTokenAt(Object.assign({}, cursor, { ch: cursor.ch + 1 }));

			return left.type === 'tag bracket' && left.string === '>'
				&& right.type === 'tag bracket' && right.string === '</';
		}
	}
}

// Compare two positions, return 0 if they are the same, a negative
// number when a is less, and a positive number otherwise.
function cmp(a, b) {
	return a.line - b.line || a.ch - b.ch;
}

function equalCursorPos(a, b) {
	return a.sticky === b.sticky && cmp(a, b) === 0;
}


'use strict';

import extract from '@emmetio/extract-abbreviation';
import createConfig from './config';
import { isCSSPropertyValue, getCSSPropertyName } from './utils';

/**
 * Context-aware abbreviation extraction from given editor.
 * Detects syntax context in `pos` editor location and, if it allows Emmet
 * abbreviation to be extracted here, returns object with extracted abbreviation,
 * its location and config.
 * @param {CodeMirror.Editor} editor
 * @param {CodeMirror.Position} pos
 */
export default function extractAbbreviation(editor, pos, contextAware) {
	const config = createConfig(editor, pos);

	if (contextAware && !canExtract(editor, pos, config)) {
		return null;
	}

	const extracted = extract(editor.getLine(pos.line), pos.ch, {
		lookAhead: true,
		syntax: config.type,
		prefix: config.syntax === 'jsx' && editor.getOption('jsxBracket') ? '<' : ''
	});

	if (extracted) {
		const from = {
			line: pos.line,
			ch: extracted.start
		};
		const to = {
			line: pos.line,
			ch: extracted.end
		};

		if (config.type === 'stylesheet' && contextAware) {
			// In case of stylesheet syntaxes (CSS, LESS) we should narrow down
			// expand context to property value, if possible
			if (isCSSPropertyValue(editor, pos)) {
				config.options = Object.assign({ property: getCSSPropertyName(editor, pos) }, config.options);
			}
		}

		return {
			abbreviation: extracted.abbreviation,
			range: { from, to },
			config
		};
	}
}

/**
 * Check if abbreviation can be extracted from given position
 * @param {CodeMirror.Editor} editor
 * @param {CodeMirror.Position} pos
 * @param {Object} config
 * @return {Boolean}
 */
function canExtract(editor, pos, config) {
	const tokenType = editor.getTokenTypeAt(pos);

	if (config.type === 'stylesheet') {
		return tokenType !== 'comment' && tokenType !== 'string';
	}

	if (config.syntax === 'html') {
		return tokenType === null;
	}

	if (config.syntax === 'slim' || config.syntax === 'pug') {
		return tokenType === null || tokenType === 'tag'
			|| (tokenType && /attribute/.test(tokenType));
	}

	if (config.syntax === 'haml') {
		return tokenType === null || tokenType === 'attribute';
	}

	if (config.syntax === 'jsx') {
		// JSX a bit tricky, delegate it to caller
		return true;
	}

	return false;
}


'use strict';

import { createSnippetsRegistry, expand } from '@emmetio/expand-abbreviation';
import { convertToCSSSnippets } from '@emmetio/css-snippets-resolver';
import createConfig from './config';
import getAbbreviation, { clearMarkers } from './abbreviation';
import { removeFields } from './utils';
import insertSnippet from './snippet';

/**
 * Returns available completions from given editor
 * @param  {CodeMirror.Editor} editor
 * @param  {Abbreviation} abbrModel Parsed Emmet abbreviation model for which
 * completions should be populated
 * @param  {CodeMirror.Position} abbrPos Abbreviation location in editor
 * @param  {CodeMirror.Position} [pos] Cursor position in editor
 * @return {EmmetCompletion[]}
 */
export default function(editor, pos) {
	pos = pos || editor.getCursor();
	let completions = [];

	// Provide two types of completions:
	// 1. Expanded abbreviation
	// 2. Snippets

	const abbreviation = getAbbreviation(editor, pos, true);
	// NB: Check for edge case: expanded abbreviation equals to original
	// abbreviation (for example, `li.item` expands to `li.item` in Slim),
	// no need to provide completion for this case
	if (abbreviation && abbreviation.abbreviation !== abbreviation.snippet) {
		completions.push(expandedAbbreviationCompletion(editor, pos, abbreviation));
	}

	const config = abbreviation ? abbreviation.config : createConfig(editor, pos);

	if (config.type === 'stylesheet') {
		completions = completions.concat(getStylesheetCompletions(editor, pos, config));
	} else {
		completions = completions.concat(getMarkupCompletions(editor, pos, config));
	}

	return {
		type: config.type,
		syntax: config.syntax,
		abbreviation,
		completions: completions.filter(Boolean)
	};
}

/**
 * Returns completions for markup syntaxes (HTML, Slim, Pug etc.)
 * @param  {CodeMirror} editor
 * @param  {CodeMirror.Position} pos Cursor position in editor
 * @param  {Object} config Resolved Emmet config
 * @return {EmmetCompletion[]}
 */
function getMarkupCompletions(editor, pos, config) {
	const line = editor.getLine(pos.line).slice(0, pos.ch);
	const prefix = extractPrefix(line, /[\w:\-$@]/);

	// Make sure that current position precedes element name (e.g. not attribute,
	// class, id etc.)
	if (prefix) {
		const prefixRange = {
			from: { line: pos.line, ch: pos.ch - prefix.length },
			to: pos
		};

		return getSnippetCompletions(editor, pos, config)
			.filter(completion => completion.key !== prefix && completion.key.indexOf(prefix) === 0)
			.map(completion => new EmmetCompletion('snippet', editor, prefixRange, completion.key, completion.preview, completion.snippet));
	}

	return [];
}

/**
 * Returns completions for stylesheet syntaxes
 * @param  {CodeMirror} editor
 * @param  {CodeMirror.Position} pos Cursor position in editor
 * @param  {Object} config Resolved Emmet config
 * @return {EmmetCompletion[]}
 */
function getStylesheetCompletions(editor, pos, config) {
	const line = editor.getLine(pos.line).slice(0, pos.ch);
	const prefix = extractPrefix(line, /[\w-@$]/);

	if (prefix) {
		// Make sure that current position precedes element name (e.g. not attribute,
		// class, id etc.)
		const prefixRange = {
			from: { line: pos.line, ch: pos.ch - prefix.length },
			to: pos
		};

		if (config.options && config.options.property) {
			const lowerProp = config.options.property.toLowerCase();
			// Find matching CSS property snippet for keyword completions
			const completion = getSnippetCompletions(editor, pos, config)
				.find(item => item.property && item.property === lowerProp);

			if (completion && completion.keywords.length) {
				return completion.keywords.map(kw => {
					return kw.key.indexOf(prefix) === 0 && new EmmetCompletion('value', editor, prefixRange, kw.key, kw.preview, kw.snippet);
				}).filter(Boolean);
			}
		} else {
			return getSnippetCompletions(editor, pos, config)
				.filter(completion => completion.key !== prefix && completion.key.indexOf(prefix) === 0)
				.map(completion => new EmmetCompletion('snippet', editor, prefixRange, completion.key, completion.preview, completion.snippet));
		}
	}

	return [];
}

/**
 * Returns all possible snippets completions for given editor context.
 * Completions are cached in editor for for re-use
 * @param  {CodeMirror.Editor} editor
 * @param  {CodeMirror.Position} pos
 * @param  {Object} config
 * @return {Array}
 */
function getSnippetCompletions(editor, pos, config) {
	const { type, syntax } = config;

	if (!editor.state.emmetCompletions) {
		editor.state.emmetCompletions = {};
	}

	const cache = editor.state.emmetCompletions;

	if (!(syntax in cache)) {
		const registry = createSnippetsRegistry(type, syntax, config.snippets);

		cache[syntax] = type === 'stylesheet'
			? getStylesheetSnippets(registry, config)
			: getMarkupSnippets(registry, config);
	}

	return cache[syntax];
}

/**
 * Returns stylesheet snippets list
 * @param {SnippetsRegistry} registry
 * @return {Array}
 */
function getStylesheetSnippets(registry) {
	return convertToCSSSnippets(registry).map(snippet => {
		let preview = snippet.property;
		const keywords = snippet.keywords();
		if (keywords.length) {
			preview += `: ${removeFields(keywords.join(' | '))}`;
		} else if (snippet.value) {
			preview += `: ${removeFields(snippet.value)}`;
		}

		return {
			key: snippet.key,
			value: snippet.value,
			snippet: snippet.key,
			property: snippet.property,
			keywords: keywords.map(kw => {
				const m = kw.match(/^[\w-]+/);
				return m && {
					key: m[0],
					preview: removeFields(kw),
					snippet: kw
				};
			}).filter(Boolean),
			preview
		};
	});
}

/**
 * Returns markup snippets list
 * @param {SnippetsRegistry} registry
 * @param {Object} config
 * @return {Array}
 */
function getMarkupSnippets(registry, config) {
	return registry.all({ type: 'string' }).map(snippet => ({
		key: snippet.key,
		value: snippet.value,
		preview: removeFields(expand(snippet.value, config)),
		snippet: snippet.key
	}));
}

function expandedAbbreviationCompletion(editor, pos, abbrModel) {
	let preview = abbrModel.preview;
	if (preview.length > 500) {
		preview = preview.slice(0, 500) + '...';
	}

	return new EmmetCompletion('expanded-abbreviation', editor, abbrModel.range,
		'Expand abbreviation', preview, (editor, range) => abbrModel.insert(editor, range));
}

/**
 * Extracts prefix from the end of given string that matches `match` regexp
 * @param {String} str
 * @param {RegExp} match
 * @return {String} Extracted prefix
 */
function extractPrefix(str, match) {
	let offset = str.length;

	while (offset > 0) {
		if (!match.test(str[offset - 1])) {
			break;
		}
		offset--;
	}

	return str.slice(offset);
}

class EmmetCompletion {
	/**
	 * @param {String} type
	 * @param {CodeMirror.Editor} editor
	 * @param {CodeMirror.Range} range
	 * @param {String} name
	 * @param {String} preview
	 * @param {Function} snippet
	 */
	constructor(type, editor, range, name, preview, snippet) {
		this.type = type;
		this.editor = editor;
		this.range = range;
		this.name = name;
		this.preview = preview;
		this.snippet = snippet;

		this._inserted = false;
	}

	insert() {
		if (!this._inserted) {
			this._inserted = true;
			if (typeof this.snippet === 'function') {
				this.snippet(this.editor, this.range);
			} else {
				insertSnippet(this.editor, this.range, this.snippet);
			}
			clearMarkers(this.editor);
		}
	}
}


'use strict';

import { expand, parse } from '@emmetio/expand-abbreviation';
import extract from './extract-abbreviation';
import insertSnippet from './snippet';
import { removeFields, isCSSPropertyValue } from './utils';

const emmetMarkerClass = 'emmet-abbreviation';

/**
 * Returns parsed abbreviation from given position in `editor`, if possible.
 * @param {CodeMirror.Editor} editor
 * @param {CodeMirror.Position} pos
 * @param {Boolean} [contextAware] Use context-aware abbreviation detection
 * @returns {Abbreviation}
 */
export default function abbreviationFromPosition(editor, pos, contextAware) {
	// Try to find abbreviation marker from given position
	const marker = findMarker(editor, pos);
	if (marker && marker.model) {
		return marker.model;
	}

	// Try to extract abbreviation from given position
	const extracted = extract(editor, pos, contextAware);
	if (extracted) {
		try {
			const abbr = new Abbreviation(extracted.abbreviation, extracted.range, extracted.config);
			return abbr.valid(editor, contextAware) ? abbr : null;
		} catch (err) {
			// skip
			// console.warn(err);
		}
	}
}

/**
 * Returns *valid* Emmet abbreviation marker (if any) for given position of editor
 * @param  {CodeMirror.Editor} editor
 * @param  {CodeMirror.Position} [pos]
 * @return {CodeMirror.TextMarker}
 */
export function findMarker(editor, pos) {
	const markers = editor.findMarksAt(pos);
	for (let i = 0, marker; i < markers.length; i++) {
		marker = markers[i];
		if (marker.className === emmetMarkerClass) {
			if (isValidMarker(editor, marker)) {
				return marker;
			}

			marker.clear();
		}
	}
}

/**
 * Removes Emmet abbreviation markers from given editor
 * @param {CodeMirror.Editor} editor
 */
export function clearMarkers(editor) {
	const markers = editor.getAllMarks();
	for (let i = 0; i < markers.length; i++) {
		if (markers[i].className === emmetMarkerClass) {
			markers[i].clear();
		}
	}
}

/**
 * Marks Emmet abbreviation for given editor position, if possible
 * @param  {CodeMirror.Editor} editor Editor where abbreviation marker should be created
 * @param  {Abbreviation} model Parsed abbreviation model
 * @return {CodeMirror.TextMarker} Returns `undefined` if no valid abbreviation under caret
 */
export function createMarker(editor, model) {
	const { from, to } = model.range;
	const marker = editor.markText(from, to, {
		inclusiveLeft: true,
		inclusiveRight: true,
		clearWhenEmpty: true,
		className: emmetMarkerClass
	});
	marker.model = model;
	return marker;
}

/**
 * Ensures that given editor Emmet abbreviation marker contains valid Emmet abbreviation
 * and updates abbreviation model if required
 * @param {CodeMirror} editor
 * @param {CodeMirror.TextMarket} marker
 * @return {Boolean} `true` if marker contains valid abbreviation
 */
function isValidMarker(editor, marker) {
	const range = marker.find();

	// No newlines inside abbreviation
	if (range.from.line !== range.to.line) {
		return false;
	}

	// Make sure marker contains valid abbreviation
	let text = editor.getRange(range.from, range.to);
	if (!text || /^\s|\s$/g.test(text)) {
		return false;
	}

	if (marker.model && marker.model.config.syntax === 'jsx' && text[0] === '<') {
		text = text.slice(1);
	}

	if (!marker.model || marker.model.abbreviation !== text) {
		// marker contents was updated, re-parse abbreviation
		try {
			marker.model = new Abbreviation(text, range, marker.model.config);
			if (!marker.model.valid(editor, true)) {
				marker.model = null;
			}
		} catch (err) {
			console.warn(err);
			marker.model = null;
		}
	}

	return Boolean(marker.model && marker.model.snippet);
}

export class Abbreviation {
	/**
	 * @param {String} abbreviation Abbreviation string
	 * @param {CodeMirror.Range} range Abbreviation location in editor
	 * @param {Object} [config]
	 */
	constructor(abbreviation, range, config) {
		this.abbreviation = abbreviation;
		this.range = range;
		this.config = config;
		this.ast = parse(abbreviation, config);
		this.snippet = expand(this.ast, config);
		this.preview = removeFields(this.snippet);
	}

	/**
	 * Inserts current expanded abbreviation into given `editor` by replacing
	 * `range`
	 * @param {CodeMirror.Editor} editor
	 * @param {CodeMirror.Range} [range]
	 */
	insert(editor, range) {
		return insertSnippet(editor, range || this.range, this.snippet);
	}

	/**
	 * Check if parsed abbreviation is valid
	 * @param {Boolean} [contextAware] Perform context-aware validation: ensure
	 * that expanded result is expected at abbreviation location
	 */
	valid(editor, contextAware) {
		if (this.preview && this.abbreviation !== this.preview) {
			return contextAware && this.config.type === 'stylesheet'
				? this._isValidForStylesheet(editor)
				: true;
		}

		return false;
	}

	_isValidForStylesheet(editor) {
		const pos = this.range.from;
		const token = editor.getTokenAt(pos);

		if (/^[#!]/.test(this.abbreviation)) {
			// Abbreviation is a property value
			return isCSSPropertyValue(editor, pos);
		}

		// All expanded nodes are properties? Properties has names, regular snippets don’t.
		const isProperty = this.ast.children.every(node => node.name);
		const state = token.state && token.state.localState || token.state;

		if (isProperty) {
			// Expanded abbreviation consists of properties: make sure we’re inside
			// block context
			// NB: in Sass, no actual block context since it’s indetation-based
			return this.config.syntax === 'sass'
				|| (state && state.context && state.context.type === 'block');
		}

		// Expanded abbreviations are basic snippets: allow them everywhere, but forbid
		// if expanded result equals abbreviation (meaningless).
		return true;
	}
}


'use strict';
/*$AMPERSAND_VERSION*/
var uniqueId = require('lodash/uniqueId');
var assign = require('lodash/assign');
var cloneObj = function(obj) { return assign({}, obj); };
var omit = require('lodash/omit');
var escape = require('lodash/escape');
var forOwn = require('lodash/forOwn');
var includes = require('lodash/includes');
var isString = require('lodash/isString');
var isObject = require('lodash/isObject');
var isDate = require('lodash/isDate');
var isFunction = require('lodash/isFunction');
var _isEqual = require('lodash/isEqual'); // to avoid shadowing
var has = require('lodash/has');
var result = require('lodash/result');
var union = require('lodash/union');
var Events = require('ampersand-events');
var KeyTree = require('key-tree-store');
var arrayNext = require('array-next');
var changeRE = /^change:/;
var noop = function () {};

function Base(attrs, options) {
    options || (options = {});
    this.cid || (this.cid = uniqueId('state'));
    this._events = {};
    this._values = {};
    this._eventBubblingHandlerCache = {};
    this._definition = Object.create(this._definition);
    if (options.parse) attrs = this.parse(attrs, options);
    this.parent = options.parent;
    this.collection = options.collection;
    this._keyTree = new KeyTree();
    this._initCollections();
    this._initChildren();
    this._cache = {};
    this._previousAttributes = {};
    if (attrs) this.set(attrs, assign({silent: true, initial: true}, options));
    this._changed = {};
    if (this._derived) this._initDerived();
    if (options.init !== false) this.initialize.apply(this, arguments);
}

assign(Base.prototype, Events, {
    // can be allow, ignore, reject
    extraProperties: 'ignore',

    idAttribute: 'id',

    namespaceAttribute: 'namespace',

    typeAttribute: 'modelType',

    // Stubbed out to be overwritten
    initialize: function () {
        return this;
    },

    // Get ID of model per configuration.
    // Should *always* be how ID is determined by other code.
    getId: function () {
        return this[this.idAttribute];
    },

    // Get namespace of model per configuration.
    // Should *always* be how namespace is determined by other code.
    getNamespace: function () {
        return this[this.namespaceAttribute];
    },

    // Get type of model per configuration.
    // Should *always* be how type is determined by other code.
    getType: function () {
        return this[this.typeAttribute];
    },

    // A model is new if it has never been saved to the server, and lacks an id.
    isNew: function () {
        return this.getId() == null;
    },

    // get HTML-escaped value of attribute
    escape: function (attr) {
        return escape(this.get(attr));
    },

    // Check if the model is currently in a valid state.
    isValid: function (options) {
        return this._validate({}, assign(options || {}, { validate: true }));
    },

    // Parse can be used remap/restructure/rename incoming properties
    // before they are applied to attributes.
    parse: function (resp, options) {
        //jshint unused:false
        return resp;
    },

    // Serialize is the inverse of `parse` it lets you massage data
    // on the way out. Before, sending to server, for example.
    serialize: function (options) {
        var attrOpts = assign({props: true}, options);
        var res = this.getAttributes(attrOpts, true);

        var setFromSerializedValue = function (value, key) {
	        res[key] = this[key].serialize();
        }.bind(this);

        forOwn(this._children, setFromSerializedValue);
        forOwn(this._collections, setFromSerializedValue);
        return res;
    },

    // Main set method used by generated setters/getters and can
    // be used directly if you need to pass options or set multiple
    // properties at once.
    set: function (key, value, options) {
        var self = this;
        var extraProperties = this.extraProperties;
        var wasChanging, changeEvents, newType, newVal, def, cast, err, attr,
            attrs, dataType, silent, unset, currentVal, initial, hasChanged, isEqual, onChange;

        // Handle both `"key", value` and `{key: value}` -style arguments.
        if (isObject(key) || key === null) {
            attrs = key;
            options = value;
        } else {
            attrs = {};
            attrs[key] = value;
        }

        options = options || {};

        if (!this._validate(attrs, options)) return false;

        // Extract attributes and options.
        unset = options.unset;
        silent = options.silent;
        initial = options.initial;

        // Initialize change tracking.
        wasChanging = this._changing;
        this._changing = true;
        changeEvents = [];

        // if not already changing, store previous
        if (initial) {
            this._previousAttributes = {};
        } else if (!wasChanging) {
            this._previousAttributes = this.attributes;
            this._changed = {};
        }

        // For each `set` attribute...
        for (var i = 0, keys = Object.keys(attrs), len = keys.length; i < len; i++) {
            attr = keys[i];
            newVal = attrs[attr];
            newType = typeof newVal;
            currentVal = this._values[attr];
            def = this._definition[attr];

            if (!def) {
                // if this is a child model or collection
                if (this._children[attr] || this._collections[attr]) {
                    if (!isObject(newVal)) {
                        newVal = {};
                    }

                    this[attr].set(newVal, options);
                    continue;
                } else if (extraProperties === 'ignore') {
                    continue;
                } else if (extraProperties === 'reject') {
                    throw new TypeError('No "' + attr + '" property defined on ' + (this.type || 'this') + ' model and extraProperties not set to "ignore" or "allow"');
                } else if (extraProperties === 'allow') {
                    def = this._createPropertyDefinition(attr, 'any');
                } else if (extraProperties) {
                    throw new TypeError('Invalid value for extraProperties: "' + extraProperties + '"');
                }
            }

            isEqual = this._getCompareForType(def.type);
            onChange = this._getOnChangeForType(def.type);
            dataType = this._dataTypes[def.type];

            // check type if we have one
            if (dataType && dataType.set) {
                cast = dataType.set(newVal);
                newVal = cast.val;
                newType = cast.type;
            }

            // If we've defined a test, run it
            if (def.test) {
                err = def.test.call(this, newVal, newType);
                if (err) {
                    throw new TypeError('Property \'' + attr + '\' failed validation with error: ' + err);
                }
            }

            // If we are required but undefined, throw error.
            // If we are null and are not allowing null, throw error
            // If we have a defined type and the new type doesn't match, and we are not null, throw error.
            // If we require specific value and new one is not one of them, throw error (unless it has default value or we're unsetting it with undefined).

            if (newVal === undefined && def.required) {
                throw new TypeError('Required property \'' + attr + '\' must be of type ' + def.type + '. Tried to set ' + newVal);
            }
            if (newVal === null && def.required && !def.allowNull) {
                throw new TypeError('Property \'' + attr + '\' must be of type ' + def.type + ' (cannot be null). Tried to set ' + newVal);
            }
            if ((def.type && def.type !== 'any' && def.type !== newType) && newVal !== null && newVal !== undefined) {
                throw new TypeError('Property \'' + attr + '\' must be of type ' + def.type + '. Tried to set ' + newVal);
            }
            if (def.values && !includes(def.values, newVal)) {
                var defaultValue = result(def, 'default');
                if (unset && defaultValue !== undefined) {
                    newVal = defaultValue;
                } else if (!unset || (unset && newVal !== undefined)) {
                    throw new TypeError('Property \'' + attr + '\' must be one of values: ' + def.values.join(', ') + '. Tried to set ' + newVal);
                }
            }

            // We know this has 'changed' if it's the initial set, so skip a potentially expensive isEqual check.
            hasChanged = initial || !isEqual(currentVal, newVal, attr);

            // enforce `setOnce` for properties if set
            if (def.setOnce && currentVal !== undefined && hasChanged) {
                throw new TypeError('Property \'' + attr + '\' can only be set once.');
            }

            // set/unset attributes.
            // If this is not the initial set, keep track of changed attributes
            // and push to changeEvents array so we can fire events.
            if (hasChanged) {

                // This fires no matter what, even on initial set.
                onChange(newVal, currentVal, attr);

                // If this is a change (not an initial set), mark the change.
                // Note it's impossible to unset on the initial set (it will already be unset),
                // so we only include that logic here.
                if (!initial) {
                    this._changed[attr] = newVal;
                    this._previousAttributes[attr] = currentVal;
                    if (unset) {
                        // FIXME delete is very slow. Can we get away with setting to undefined?
                        delete this._values[attr];
                    }
                    if (!silent) {
                        changeEvents.push({prev: currentVal, val: newVal, key: attr});
                    }
                }
                if (!unset) {
                    this._values[attr] = newVal;
                }
            } else {
                // Not changed
                // FIXME delete is very slow. Can we get away with setting to undefined?
                delete this._changed[attr];
            }
        }

        // Fire events. This array is not populated if we are told to be silent.
        if (changeEvents.length) this._pending = true;
        changeEvents.forEach(function (change) {
            self.trigger('change:' + change.key, self, change.val, options);
        });

        // You might be wondering why there's a `while` loop here. Changes can
        // be recursively nested within `"change"` events.
        if (wasChanging) return this;
        while (this._pending) {
            this._pending = false;
            this.trigger('change', this, options);
        }
        this._pending = false;
        this._changing = false;
        return this;
    },

    get: function (attr) {
        return this[attr];
    },

    // Toggle boolean properties or properties that have a `values`
    // array in its definition.
    toggle: function (property) {
        var def = this._definition[property];
        if (def.type === 'boolean') {
            // if it's a bool, just flip it
            this[property] = !this[property];
        } else if (def && def.values) {
            // If it's a property with an array of values
            // skip to the next one looping back if at end.
            this[property] = arrayNext(def.values, this[property]);
        } else {
            throw new TypeError('Can only toggle properties that are type `boolean` or have `values` array.');
        }
        return this;
    },

    // Get all of the attributes of the model at the time of the previous
    // `"change"` event.
    previousAttributes: function () {
        return cloneObj(this._previousAttributes);
    },

    // Determine if the model has changed since the last `"change"` event.
    // If you specify an attribute name, determine if that attribute has changed.
    hasChanged: function (attr) {
        if (attr == null) return !!Object.keys(this._changed).length;
        if (has(this._derived, attr)) {
            return this._derived[attr].depList.some(function (dep) {
                return this.hasChanged(dep);
            }, this);
        }
        return has(this._changed, attr);
    },

    // Return an object containing all the attributes that have changed, or
    // false if there are no changed attributes. Useful for determining what
    // parts of a view need to be updated and/or what attributes need to be
    // persisted to the server. Unset attributes will be set to undefined.
    // You can also pass an attributes object to diff against the model,
    // determining if there *would be* a change.
    changedAttributes: function (diff) {
        if (!diff) return this.hasChanged() ? cloneObj(this._changed) : false;
        var val, changed = false;
        var old = this._changing ? this._previousAttributes : this.attributes;
        var def, isEqual;
        for (var attr in diff) {
            def = this._definition[attr];
            if (!def) continue;
            isEqual = this._getCompareForType(def.type);
            if (isEqual(old[attr], (val = diff[attr]))) continue;
            (changed || (changed = {}))[attr] = val;
        }
        return changed;
    },

    toJSON: function () {
        return this.serialize();
    },

    unset: function (attrs, options) {
        var self = this;
        attrs = Array.isArray(attrs) ? attrs : [attrs];
        attrs.forEach(function (key) {
            var def = self._definition[key];
            if (!def) return;
            var val;
            if (def.required) {
                val = result(def, 'default');
                return self.set(key, val, options);
            } else {
                return self.set(key, val, assign({}, options, {unset: true}));
            }
        });
    },

    clear: function (options) {
        var self = this;
        Object.keys(this.attributes).forEach(function (key) {
            self.unset(key, options);
        });
        return this;
    },

    previous: function (attr) {
        if (attr == null || !Object.keys(this._previousAttributes).length) return null;
        return this._previousAttributes[attr];
    },

    // Get default values for a certain type
    _getDefaultForType: function (type) {
        var dataType = this._dataTypes[type];
        return dataType && dataType['default'];
    },

    // Determine which comparison algorithm to use for comparing a property
    _getCompareForType: function (type) {
        var dataType = this._dataTypes[type];
        if (dataType && dataType.compare) return dataType.compare.bind(this);
        return _isEqual; // if no compare function is defined, use _.isEqual
    },

    _getOnChangeForType : function(type){
        var dataType = this._dataTypes[type];
        if (dataType && dataType.onChange) return dataType.onChange.bind(this);
        return noop;
    },

    // Run validation against the next complete set of model attributes,
    // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
    _validate: function (attrs, options) {
        if (!options.validate || !this.validate) return true;
        attrs = assign({}, this.attributes, attrs);
        var error = this.validationError = this.validate(attrs, options) || null;
        if (!error) return true;
        this.trigger('invalid', this, error, assign(options || {}, {validationError: error}));
        return false;
    },

    _createPropertyDefinition: function (name, desc, isSession) {
        return createPropertyDefinition(this, name, desc, isSession);
    },

    // just makes friendlier errors when trying to define a new model
    // only used when setting up original property definitions
    _ensureValidType: function (type) {
        return includes(['string', 'number', 'boolean', 'array', 'object', 'date', 'state', 'any']
            .concat(Object.keys(this._dataTypes)), type) ? type : undefined;
    },

    getAttributes: function (options, raw) {
        options = assign({
            session: false,
            props: false,
            derived: false
        }, options || {});
        var res = {};
        var val, def;
        for (var item in this._definition) {
            def = this._definition[item];
            if ((options.session && def.session) || (options.props && !def.session)) {
                val = raw ? this._values[item] : this[item];
                if (raw && val && isFunction(val.serialize)) val = val.serialize();
                if (typeof val === 'undefined') val = result(def, 'default');
                if (typeof val !== 'undefined') res[item] = val;
            }
        }
        if (options.derived) {
            for (var derivedItem in this._derived) res[derivedItem] = this[derivedItem];
        }
        return res;
    },

    _initDerived: function () {
        var self = this;

        forOwn(this._derived, function (value, name) {
            var def = self._derived[name];
            def.deps = def.depList;

            var update = function () {
                var newVal = def.fn.call(self);

                if (self._cache[name] !== newVal || !def.cache) {
                    if (def.cache) {
                        self._previousAttributes[name] = self._cache[name];
                    }
                    self._cache[name] = newVal;
                    self.trigger('change:' + name, self, self._cache[name]);
                }
            };

            def.deps.forEach(function (propString) {
                self._keyTree.add(propString, update);
            });
        });

        this.on('all', function (eventName) {
            if (changeRE.test(eventName)) {
                self._keyTree.get(eventName.split(':')[1]).forEach(function (fn) {
                    fn();
                });
            }
        }, this);
    },

    _getDerivedProperty: function (name, flushCache) {
        // is this a derived property that is cached
        if (this._derived[name].cache) {
            //set if this is the first time, or flushCache is set
            if (flushCache || !this._cache.hasOwnProperty(name)) {
                this._cache[name] = this._derived[name].fn.apply(this);
            }
            return this._cache[name];
        } else {
            return this._derived[name].fn.apply(this);
        }
    },

    _initCollections: function () {
        var coll;
        if (!this._collections) return;
        for (coll in this._collections) {
            this._safeSet(coll, new this._collections[coll](null, {parent: this}));
        }
    },

    _initChildren: function () {
        var child;
        if (!this._children) return;
        for (child in this._children) {
            this._safeSet(child, new this._children[child]({}, {parent: this}));
            this.listenTo(this[child], 'all', this._getCachedEventBubblingHandler(child));
        }
    },

    // Returns a bound handler for doing event bubbling while
    // adding a name to the change string.
    _getCachedEventBubblingHandler: function (propertyName) {
        if (!this._eventBubblingHandlerCache[propertyName]) {
            this._eventBubblingHandlerCache[propertyName] = function (name, model, newValue) {
                if (changeRE.test(name)) {
                    this.trigger('change:' + propertyName + '.' + name.split(':')[1], model, newValue);
                } else if (name === 'change') {
                    this.trigger('change', this);
                }
            }.bind(this);
        }
        return this._eventBubblingHandlerCache[propertyName];
    },

    // Check that all required attributes are present
    _verifyRequired: function () {
        var attrs = this.attributes; // should include session
        for (var def in this._definition) {
            if (this._definition[def].required && typeof attrs[def] === 'undefined') {
                return false;
            }
        }
        return true;
    },

    // expose safeSet method
    _safeSet: function safeSet(property, value) {
        if (property in this) {
            throw new Error('Encountered namespace collision while setting instance property `' + property + '`');
        }
        this[property] = value;
        return this;
    }
});

// getter for attributes
Object.defineProperties(Base.prototype, {
    attributes: {
        get: function () {
            return this.getAttributes({props: true, session: true});
        }
    },
    all: {
        get: function () {
            return this.getAttributes({
                session: true,
                props: true,
                derived: true
            });
        }
    },
    isState: {
        get: function () { return true; },
        set: function () { }
    }
});

// helper for creating/storing property definitions and creating
// appropriate getters/setters
function createPropertyDefinition(object, name, desc, isSession) {
    var def = object._definition[name] = {};
    var type, descArray;

    if (isString(desc)) {
        // grab our type if all we've got is a string
        type = object._ensureValidType(desc);
        if (type) def.type = type;
    } else {
        //Transform array of ['type', required, default] to object form
        if (Array.isArray(desc)) {
            descArray = desc;
            desc = {
                type: descArray[0],
                required: descArray[1],
                'default': descArray[2]
            };
        }

        type = object._ensureValidType(desc.type);
        if (type) def.type = type;

        if (desc.required) def.required = true;

        if (desc['default'] && typeof desc['default'] === 'object') {
            throw new TypeError('The default value for ' + name + ' cannot be an object/array, must be a value or a function which returns a value/object/array');
        }

        def['default'] = desc['default'];

        def.allowNull = desc.allowNull ? desc.allowNull : false;
        if (desc.setOnce) def.setOnce = true;
        if (def.required && def['default'] === undefined && !def.setOnce) def['default'] = object._getDefaultForType(type);
        def.test = desc.test;
        def.values = desc.values;
    }
    if (isSession) def.session = true;

    if (!type) {
        type = isString(desc) ? desc : desc.type;
        // TODO: start throwing a TypeError in future major versions instead of warning
        console.warn('Invalid data type of `' + type + '` for `' + name + '` property. Use one of the default types or define your own');
    }

    // define a getter/setter on the prototype
    // but they get/set on the instance
    Object.defineProperty(object, name, {
        set: function (val) {
            this.set(name, val);
        },
        get: function () {
            if (!this._values) {
                throw Error('You may be trying to `extend` a state object with "' + name + '" which has been defined in `props` on the object being extended');
            }
            var value = this._values[name];
            var typeDef = this._dataTypes[def.type];
            if (typeof value !== 'undefined') {
                if (typeDef && typeDef.get) {
                    value = typeDef.get(value);
                }
                return value;
            }
            var defaultValue = result(def, 'default');
            this._values[name] = defaultValue;
            // If we've set a defaultValue, fire a change handler effectively marking
            // its change from undefined to the default value.
            if (typeof defaultValue !== 'undefined') {
                var onChange = this._getOnChangeForType(def.type);
                onChange(defaultValue, value, name);
            }
            return defaultValue;
        }
    });

    return def;
}

// helper for creating derived property definitions
function createDerivedProperty(modelProto, name, definition) {
    var def = modelProto._derived[name] = {
        fn: isFunction(definition) ? definition : definition.fn,
        cache: (definition.cache !== false),
        depList: definition.deps || []
    };

    // add to our shared dependency list
    def.depList.forEach(function (dep) {
        modelProto._deps[dep] = union(modelProto._deps[dep] || [], [name]);
    });

    // defined a top-level getter for derived names
    Object.defineProperty(modelProto, name, {
        get: function () {
            return this._getDerivedProperty(name);
        },
        set: function () {
            throw new TypeError("`" + name + "` is a derived property, it can't be set directly.");
        }
    });
}

var dataTypes = {
    string: {
        'default': function () {
            return '';
        }
    },
    date: {
        set: function (newVal) {
            var newType;
            if (newVal == null) {
                newType = typeof null;
            } else if (!isDate(newVal)) {
                var err = null;
                var dateVal = new Date(newVal).valueOf();
                if (isNaN(dateVal)) {
                    // If the newVal cant be parsed, then try parseInt first
                    dateVal = new Date(parseInt(newVal, 10)).valueOf();
                    if (isNaN(dateVal)) err = true;
                }
                newVal = dateVal;
                newType = 'date';
                if (err) {
                    newType = typeof newVal;
                }
            } else {
                newType = 'date';
                newVal = newVal.valueOf();
            }

            return {
                val: newVal,
                type: newType
            };
        },
        get: function (val) {
            if (val == null) { return val; }
            return new Date(val);
        },
        'default': function () {
            return new Date();
        }
    },
    array: {
        set: function (newVal) {
            return {
                val: newVal,
                type: Array.isArray(newVal) ? 'array' : typeof newVal
            };
        },
        'default': function () {
            return [];
        }
    },
    object: {
        set: function (newVal) {
            var newType = typeof newVal;
            // we have to have a way of supporting "missing" objects.
            // Null is an object, but setting a value to undefined
            // should work too, IMO. We just override it, in that case.
            if (newType !== 'object' && newVal === undefined) {
                newVal = null;
                newType = 'object';
            }
            return {
                val: newVal,
                type: newType
            };
        },
        'default': function () {
            return {};
        }
    },
    // the `state` data type is a bit special in that setting it should
    // also bubble events
    state: {
        set: function (newVal) {
            var isInstance = newVal instanceof Base || (newVal && newVal.isState);
            if (isInstance) {
                return {
                    val: newVal,
                    type: 'state'
                };
            } else {
                return {
                    val: newVal,
                    type: typeof newVal
                };
            }
        },
        compare: function (currentVal, newVal) {
            return currentVal === newVal;
        },

        onChange : function(newVal, previousVal, attributeName){
            // if this has changed we want to also handle
            // event propagation
            if (previousVal) {
                this.stopListening(previousVal, 'all', this._getCachedEventBubblingHandler(attributeName));
            }

            if (newVal != null) {
                this.listenTo(newVal, 'all', this._getCachedEventBubblingHandler(attributeName));
            }
        }
    }
};

// the extend method used to extend prototypes, maintain inheritance chains for instanceof
// and allow for additions to the model definitions.
function extend(protoProps) {
    /*jshint validthis:true*/
    var parent = this;
    var child;

    // The constructor function for the new subclass is either defined by you
    // (the "constructor" property in your `extend` definition), or defaulted
    // by us to simply call the parent's constructor.
    if (protoProps && protoProps.hasOwnProperty('constructor')) {
        child = protoProps.constructor;
    } else {
        child = function () {
            return parent.apply(this, arguments);
        };
    }

    // Add static properties to the constructor function from parent
    assign(child, parent);

    // Set the prototype chain to inherit from `parent`, without calling
    // `parent`'s constructor function.
    var Surrogate = function () { this.constructor = child; };
    Surrogate.prototype = parent.prototype;
    child.prototype = new Surrogate();

    // set prototype level objects
    child.prototype._derived =  assign({}, parent.prototype._derived);
    child.prototype._deps = assign({}, parent.prototype._deps);
    child.prototype._definition = assign({}, parent.prototype._definition);
    child.prototype._collections = assign({}, parent.prototype._collections);
    child.prototype._children = assign({}, parent.prototype._children);
    child.prototype._dataTypes = assign({}, parent.prototype._dataTypes || dataTypes);

    // Mix in all prototype properties to the subclass if supplied.
    if (protoProps) {
        var omitFromExtend = [
            'dataTypes', 'props', 'session', 'derived', 'collections', 'children'
        ];
        for(var i = 0; i < arguments.length; i++) {
            var def = arguments[i];
            if (def.dataTypes) {
                forOwn(def.dataTypes, function (def, name) {
                    child.prototype._dataTypes[name] = def;
                });
            }
            if (def.props) {
                forOwn(def.props, function (def, name) {
                    createPropertyDefinition(child.prototype, name, def);
                });
            }
            if (def.session) {
                forOwn(def.session, function (def, name) {
                    createPropertyDefinition(child.prototype, name, def, true);
                });
            }
            if (def.derived) {
                forOwn(def.derived, function (def, name) {
                    createDerivedProperty(child.prototype, name, def);
                });
            }
            if (def.collections) {
                forOwn(def.collections, function (constructor, name) {
                    child.prototype._collections[name] = constructor;
                });
            }
            if (def.children) {
                forOwn(def.children, function (constructor, name) {
                    child.prototype._children[name] = constructor;
                });
            }
            assign(child.prototype, omit(def, omitFromExtend));
        }
    }

    // Set a convenience property in case the parent's prototype is needed
    // later.
    child.__super__ = parent.prototype;

    return child;
}

Base.extend = extend;

// Our main exports
module.exports = Base;


'use strict'

//----------------
// Includes: Self
//----------------
var shared    = require('./2 - shared.js')
var config    = require('./3 - config.js')
var functions = require('./4 - functions.js')

//----------
// Includes
//----------
var chalk = require('chalk') // ~ 20 ms
var path  = require('path')  // ~  1 ms

//-----------
// Variables
//-----------
var clean = {}

//----------------------------
// Clean: Command and Control
//----------------------------
// The following functions control cleaning, setting up promise chains and concurrency.

clean.processClean = function clean_processClean(files, watching) {
    /*
    Remove the destination directory or start a more complex incremental cleanup.
    @param   {String,Object}  [files]     Optional. Glob search string like '*.html' or array of full paths like ['/web/dest/about.html', '/web/dest/index.html']
    @param   {Boolean}        [watching]  Optional. If true, we are in watch mode so log less information and do not republish.
    @return  {Promise}                    Promise that returns an array of file path strings for the files cleaned like ['/dest/css/style.css', '/dest/index.html']
    */
    watching = watching || false

    if (!config.option.clean && !watching) {
        return Promise.resolve()
    }

    return Promise.resolve().then(function() {

        if (!watching) {
            // start clean timer
            shared.stats.timeTo.clean = functions.sharedStatsTimeTo(shared.stats.timeTo.clean)
        }

        var configPathsAreGood = functions.configPathsAreGood()
        if (configPathsAreGood !== true) {
            throw new Error(configPathsAreGood)
        }

    }).then(function() {

        return functions.fileExists(config.path.source).then(function(exists) {
            if (exists === false) {
                throw new Error(shared.language.display('error.missingSourceDirectory'))
            }
        })

    }).then(function() {

        if (!watching) {
            // display title
            functions.log(chalk.gray('\n' + shared.language.display('words.clean') + '\n'), false)
        }

        if (config.option.republish && !watching) {
            // remove all files from inside the dest directory
            var options = {
                "nocase"  : true,
                "nodir"   : false,
                "realpath": true
            }

            return functions.findFiles(config.path.dest + '/{*,.*}', options).then(function(files) {
                return functions.removeFiles(files).then(function() {
                    return [config.path.dest]
                })
            })
        } else {
            // incremental cleanup

            var filesType = typeof files

            if (filesType === 'object') {
                // we already have a specified list to work from
                return clean.processFiles(files, watching)
            } else {
                var options = {
                    "nocase"  : true,
                    "nodir"   : false,
                    "realpath": true
                }

                if (filesType === 'string') {
                    if (files.charAt(0) === '/') {
                        files = files.replace('/', '')
                    }
                } else {
                    if (config.glob.clean !== '') {
                        files = config.glob.clean
                    } else {
                        files = '**/*'
                    }
                }

                return functions.findFiles(config.path.dest + '/' + files, options).then(function(files) {
                    if (files.length > 0) {
                        return clean.processFiles(files, watching)
                    } else {
                        return []
                    }
                })
            }
        }

    }).then(function(filesCleaned) {

        if (!watching) {
            shared.stats.timeTo.clean = functions.sharedStatsTimeTo(shared.stats.timeTo.clean)

            if (filesCleaned.length === 0) {
                functions.log(chalk.gray(shared.language.display('words.done') + '.'))
            } else if (filesCleaned[0] === config.path.dest) {
                functions.log(chalk.gray('/' + path.basename(config.path.dest) + ' ' + shared.language.display('words.removed')))
            }
        }

        return filesCleaned

    })
} // processClean

clean.processFiles = function clean_processFiles(files, watching) {
    /*
    Create a promise chain of tasks for each file and control concurrency.
    @param   {Object,String}  files  Array of paths like ['/dest/path1', '/dest/path2'] or a string like '/dest/path'
    @return  {Promise}               Promise that returns an array of file path strings for the files cleaned like ['/dest/css/style.css', '/dest/index.html']
    */
    watching = watching || false

    var filesCleaned = [] // keep track of any files cleaned

    functions.cacheReset()

    return new Promise(function(resolve, reject) {
        if (typeof files === 'string') {
            files = [files]
        }

        var allFiles = []    // array of promises
        var current  = 0     // number of operations running currently
        var resolved = false // true if all tasks have been queued

        function proceed() {
            current--

            if (files.length > 0) {
                queue()
            } else if (!resolved) {
                resolved = true
                resolve(allFiles)
            }
        } // proceed

        function queue() {
            while (current < config.concurLimit && files.length > 0) {
                var file = files.shift()

                allFiles.push(Promise.resolve(file).then(function(file) {
                    return clean.processOneClean(file).then(function(filePath) {
                        if (typeof filePath === 'string') {
                            filesCleaned.push(filePath)
                        }
                        proceed()
                    }).catch(function(err) {
                        proceed()
                        functions.logError(err)
                        throw err
                    })
                }))

                current++
            }
        } // queue

        queue()

    }).then(function(allFiles) {

        return Promise.all(allFiles).then(function() {
            return filesCleaned
        })

    })
} // processFiles

clean.processOneClean = function clean_processOneClean(filePath) {
    /*
    Create a promise chain of cleaning tasks based on a single file type.
    @param   {String}   filePath  Path like '/dest/index.html'
    @return  {Promise}            Promise that returns a file path string if something was cleaned otherwise undefined.
    */
    return new Promise(function(resolve, reject) {
        return functions.fileExists(filePath).then(function(destExists) {

            if (!destExists) {
                throw 'done'
            } else {
                var prefix = path.basename(filePath).substr(0, config.includePrefix.length)

                var fileExt = functions.fileExtension(filePath)

                if (prefix === config.includePrefix || fileExt === 'concat') {
                    // prefixed files are includes and should not be in the destination folder
                    // concat files should not be in the destination folder either
                    return functions.removeDest(filePath).then(function() {
                        throw 'done'
                    })
                } else {
                    return fileExt
                }
            }

        }).then(function(fileExt) {

            // if we got this far we know the destination file exists and it is not an include

            var obj = {
                destFile: filePath,
                destExt: fileExt,
                sourceExists: false
            }

            obj.sourceFiles = functions.possibleSourceFiles(filePath)

            var len = obj.sourceFiles.length

            var p = Promise.resolve()

            for (var i = 0; i < len; i++) {
                (function() {
                    var possibleSourceFile = obj.sourceFiles[i]

                    p = p.then(function(exists) {
                        if (exists) {
                            return true
                        } else {
                            return functions.fileExists(possibleSourceFile)
                        }
                    })
                })()
            }

            p = p.then(function(exists) {
                if (exists) obj.sourceExists = true
                return obj
            })

            return p

        }).then(function(obj) {

            if (!obj.sourceExists) {
                return functions.removeDest(obj.destFile).then(function() {
                    resolve(obj.destFile)
                }).catch(function(err) {
                    console.warn(err)
                    reject(err)
                })
            } else {
                // do not delete the destination file since some equivalent of it exists in the source folder
                resolve()
            }

        }).catch(function(err) {

            if (err === 'done') {
                resolve()
            } else {
                console.warn(err)
                reject(err)
            }

        })
    })
} // processOneClean

//---------
// Exports
//---------
module.exports = clean


'use strict'

//----------
// Includes
//----------
var expect = require('expect.js')
var fs     = require('fs')
var path   = require('path')

var shared    = require('../code/2 - shared.js')
var config    = require('../code/3 - config.js')
var functions = require('../code/4 - functions.js')
var clean     = require('../code/5 - clean.js')
var build     = require('../code/6 - build.js')
var watch     = require('../code/7 - watch.js')

//-----------
// Variables
//-----------
var configBackup = functions.cloneObj(config)
var testPath = path.join(shared.path.self, 'mocha', 'files', 'watch')
var reWriteTimer = setTimeout(function() {}, 0)

//-----------
// Functions
//-----------
var reWriter = function reWriter(goCrazy, filePath, data) {
    /*
    Keep on writing a file until chokidar notices us.
    @param  {Boolean}  goCrazy   Start or continue to write a file every 500 ms until someone stops us!
    @param  {String}   filePath  String file path like '/source/file.txt'. Not used if goCrazy is false.
    @param  {String}   [data]    Optional data to write to the file. Defaults to 'changed data'.
    */
    if (goCrazy) {
        data = data || 'changed data'
        fs.writeFileSync(filePath, data)

        reWriteTimer = setTimeout(function() {
            reWriter(goCrazy, filePath, data)
        }, 500)
    } else {
        clearTimeout(reWriteTimer)
    }
}

//------------------------
// Notes for Test Writers
//------------------------
/*
Use Unique File Names

    Specify unique source file names in tests that may use watch.notTooRecent() behind the scenes.
    If you use do not use unique source file names, you may lose events that happen in quick succession.

Chokidar + Mac = Weird

    Let's say you are using chokidar to watch a directory.
    First you create a file in said directory, witness a watch event, then immediately run functions.removeFile to remove the parent directory containing the file.
    You then receive an error like "ENOTEMPTY: directory not empty, rmdir /path/to/file".
    If you have to conpensate for this, delete files first to ensure empty folders. Then remove empty folders.
*/

//-------------
// Mocha Tests
//-------------
describe('File -> ../code/7 - watch.js\n', function() {

    beforeEach(function() {
        // runs before each test in this describe block
        config = functions.restoreObj(config, configBackup)

        config.option.concurLimit = 1
        config.option.watch = true
    })

    afterEach(function() {
        // runs after each test in this describe block

        // remove any event listeners
        watch.emitterDest.removeAllListeners()
        watch.emitterSource.removeAllListeners()

        watch.stop()
    })

    //----------------
    // watch.buildOne
    //----------------
    describe('buildOne', function() {
        it('should build one file', function() {

            config.path.source = path.join(testPath, 'buildOne', 'source')
            config.path.dest   = path.join(testPath, 'buildOne', 'dest')

            var fileSource = path.join(config.path.source, 'buildOne.txt')
            var fileDest = path.join(config.path.dest, 'buildOne.txt')

            return Promise.resolve().then(function() {

                return functions.removeFile(fileDest)

            }).then(function() {

                return watch.buildOne(fileSource)

            }).then(function() {

                return functions.fileExists(fileDest)

            }).then(function(exists) {

                expect(exists).to.be(true)

                return functions.removeFile(fileDest)

            }).then(function() {

                return functions.removeFile(config.path.dest)

            })

        }) // it
    }) // describe

    //--------------------
    // watch.notTooRecent
    //--------------------
    describe('notTooRecent', function() {
        it('should return true if file activity is spaced apart', function(done) {

            watch.notTooRecent('notTooRecent1.txt')
            setTimeout(function() {
                var bool = watch.notTooRecent('notTooRecent1.txt')
                expect(bool).to.be(true)
                done()
            }, 350)

        }) // it

        it('should return false for file activity in quick succession', function() {

            watch.notTooRecent('notTooRecent2.txt')
            var bool = watch.notTooRecent('notTooRecent2.txt')
            expect(bool).to.be(false)

        }) // it
    }) // describe

    //--------------------
    // watch.processWatch
    //--------------------
    describe('processWatch', function() {
        it('should see events for both source and destination', function() {

            config.option.livereload = true

            config.path.source = path.join(testPath, 'processWatch-1', 'source')
            config.path.dest   = path.join(testPath, 'processWatch-1', 'dest')

            var fileSource = path.join(config.path.source, 'one.html')
            var fileDest   = path.join(config.path.dest, 'one.html')

            return Promise.resolve().then(function() {

                return watch.processWatch()

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    var check1 = false
                    var check2 = false

                    function check() {
                        if (check1 && check2) {
                            resolve(true)
                        }
                    }

                    function checkSource(file) {
                        expect(file).to.be(fileSource)
                        check1 = true
                        check()
                    }

                    function checkDest(file) {
                        reWriter(false)
                        expect(file).to.be(fileDest)
                        check2 = true
                        check()
                    }

                    watch.emitterSource.on('change', checkSource)

                    watch.emitterDest.on('change', checkDest)

                    reWriter(true, fileSource)

                })

            }).then(function(checks) {

                expect(checks).to.be(true)

                watch.stop()

                return functions.writeFile(fileSource, 'original data')

            }).then(function() {

                return functions.writeFile(fileDest, 'original data')

            })

        }) // it

        it('should use config.glob.watch strings if available', function() {

            config.option.livereload = true

            config.glob.watch.source = '*.less'
            config.glob.watch.dest = '*.css'

            config.path.source = path.join(testPath, 'processWatch-2', 'source')
            config.path.dest   = path.join(testPath, 'processWatch-2', 'dest')

            var fileSource = path.join(config.path.source, 'two.less')
            var fileDest   = path.join(config.path.dest, 'two.css')

            return Promise.resolve().then(function() {

                return watch.processWatch()

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    var check1 = false
                    var check2 = false

                    function check() {
                        if (check1 && check2) {
                            resolve(true)
                        }
                    }

                    function checkSource(file) {
                        expect(file).to.be(fileSource)
                        check1 = true
                        check()
                    }

                    function checkDest(file) {
                        reWriter(false)
                        expect(file).to.be(fileDest)
                        check2 = true
                        check()
                    }

                    watch.emitterSource.on('change', checkSource)

                    watch.emitterDest.on('change', checkDest)

                    reWriter(true, fileSource, '*{content:"tastes great, less filling"}')
                })

            }).then(function(checks) {

                expect(checks).to.be(true)

                watch.stop()

                return functions.writeFile(fileSource, 'original data')

            }).then(function() {

                return functions.writeFile(fileDest, 'original data')

            })

        }) // it

        it('should use glob search strings as parameters', function() {

            config.option.livereload = true

            config.path.source = path.join(testPath, 'processWatch-3', 'source')
            config.path.dest   = path.join(testPath, 'processWatch-3', 'dest')

            var fileSource = path.join(config.path.source, 'three.js')
            var fileDest   = path.join(config.path.dest, 'three.js')

            var ignoreFileSource = path.join(config.path.source, 'three.html')
            var ignoreFileDest   = path.join(config.path.dest, 'three.html')

            return Promise.resolve().then(function() {

                return watch.processWatch('*.js', '*.js')

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    var check1 = false
                    var check2 = false

                    function check() {
                        if (check1 && check2) {
                            resolve(true)
                        }
                    }

                    function checkSource(file) {
                        expect(file).to.be(fileSource)
                        check1 = true
                        check()
                    }

                    function checkDest(file) {
                        reWriter(false)
                        expect(file).to.be(fileDest)
                        check2 = true
                        check()
                    }

                    watch.emitterSource.on('change', checkSource)

                    watch.emitterDest.on('change', checkDest)

                    return functions.writeFile(ignoreFileSource, 'changed data').then(function() {

                        reWriter(true, fileSource, 'var peanut = "butter"')

                    })

                })

            }).then(function(checks) {

                expect(checks).to.be(true)

                watch.stop()

                return functions.writeFile(fileSource, 'original data')

            }).then(function() {

                return functions.writeFile(fileDest, 'original data')

            }).then(function() {

                return functions.writeFile(ignoreFileSource, 'original data')

            }).then(function() {

                return functions.removeFile(ignoreFileDest) // this file should never be here but remove it just in case

            })

        }) // it

        it('should use arrays with file path strings as parameters', function() {

            config.option.livereload = true

            config.path.source = path.join(testPath, 'processWatch-4', 'source')
            config.path.dest   = path.join(testPath, 'processWatch-4', 'dest')

            var fileSource = path.join(config.path.source, 'four.js')
            var fileDest   = path.join(config.path.dest, 'four.js')

            var ignoreFileSource = path.join(config.path.source, 'four.html')
            var ignoreFileDest   = path.join(config.path.dest, 'four.html')

            return Promise.resolve().then(function() {

                return watch.processWatch([fileSource], [fileDest])

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    var check1 = false
                    var check2 = false

                    function check() {
                        if (check1 && check2) {
                            resolve(true)
                        }
                    }

                    function checkSource(file) {
                        expect(file).to.be(fileSource)
                        check1 = true
                        check()
                    }

                    function checkDest(file) {
                        reWriter(false)
                        expect(file).to.be(fileDest)
                        check2 = true
                        check()
                    }

                    watch.emitterSource.on('change', checkSource)

                    watch.emitterDest.on('change', checkDest)

                    return functions.writeFile(ignoreFileSource, 'changed data').then(function() {

                        reWriter(true, fileSource, 'var jelly = "time"')

                    })

                })

            }).then(function(checks) {

                expect(checks).to.be(true)

                watch.stop()

                return functions.writeFile(fileSource, 'original data')

            }).then(function() {

                return functions.writeFile(fileDest, 'original data')

            }).then(function() {

                return functions.writeFile(ignoreFileSource, 'original data')

            }).then(function() {

                return functions.removeFile(ignoreFileDest) // this file should never be here but remove it just in case

            })

        }) // it
    }) // describe

    //------------
    // watch.stop
    //------------
    // Stop watching the source and/or destination folders. Also stop the livereload server.
    // No need to test since it will be tested by watch.processWatch, watch.watchDest, and watch.watchSource.

    //------------------------------
    // watch.updateLiveReloadServer
    //------------------------------
    describe('updateLiveReloadServer', function() {
        it('should not return an error', function() {

            watch.updateLiveReloadServer('now').then(function(updated) {
                expect(updated).to.be(true)
            })

        }) // it
    }) // describe

    //-----------------
    // watch.watchDest
    //-----------------
    describe('watchDest', function() {
        it('should notice an event in the destination folder', function() {

            config.option.livereload = true

            config.path.source = path.join(testPath, 'watchDest', 'source')
            config.path.dest   = path.join(testPath, 'watchDest', 'dest')

            var fileDest = path.join(config.path.dest, 'watchDest.html')

            return Promise.resolve().then(function() {

                return watch.watchDest()

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    watch.emitterDest.on('change', function() {
                        reWriter(false)
                        resolve(true)
                    })

                    reWriter(true, fileDest)

                })

            }).then(function(emit) {

                expect(emit).to.be(true)

                return functions.writeFile(fileDest, 'original data')

            })

        }) // it
    }) // describe

    //-------------------
    // watch.watchSource
    //-------------------
    describe('watchSource', function() {
        it('should notice an event in the source folder', function() {

            config.path.source = path.join(testPath, 'watchSource', 'source')
            config.path.dest   = path.join(testPath, 'watchSource', 'dest')

            var fileSource = path.join(config.path.source, 'watchSource.html')

            return Promise.resolve().then(function() {

                return watch.watchSource()

            }).then(function() {

                return new Promise(function(resolve, reject) {

                    watch.emitterSource.on('change', function(file) {
                        reWriter(false)
                        resolve(true)
                    })

                    reWriter(true, fileSource)

                })

            }).then(function(emit) {

                expect(emit).to.be(true)

                watch.stop()

                return functions.writeFile(fileSource, 'original data')

            })

        }) // it
    }) // describe

}) // describe