return ctx.token.state.html.htmlState.tagName;  //HTML mode for PHP files

/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */

define(function (require, exports, module) {
    "use strict";

    var TokenUtils = require("utils/TokenUtils");

    //constants
    var TAG_NAME = "tagName",
        ATTR_NAME = "attr.name",
        ATTR_VALUE = "attr.value";

   /**
     * @private
     * Sometimes as attr values are getting typed, if the quotes aren't balanced yet
     * some extra 'non attribute value' text gets included in the token. This attempts
     * to assure the attribute value we grab is always good
     * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context
     * @return { val:{string}, offset:{number}}
     */
    function _extractAttrVal(ctx) {
        var attrValue = ctx.token.string,
            startChar = attrValue.charAt(0),
            endChar = attrValue.charAt(attrValue.length - 1),
            offset = TokenUtils.offsetInToken(ctx),
            foundEqualSign = false;

        //If this is a fully quoted value, return the whole
        //thing regardless of position
        if (attrValue.length > 1 &&
                (startChar === "'" || startChar === '"') &&
                endChar === startChar) {

            // Find an equal sign before the end quote. If found,
            // then the user may be entering an attribute value right before
            // another attribute and we're getting a false balanced string.
            // An example of this case is <link rel" href="foo"> where the
            // cursor is right after the first double quote.
            foundEqualSign = (attrValue.match(/\=\s*['"]$/) !== null);

            if (!foundEqualSign) {
                //strip the quotes and return;
                attrValue = attrValue.substring(1, attrValue.length - 1);
                offset = offset - 1 > attrValue.length ? attrValue.length : offset - 1;
                return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: true};
            }
        }

        if (foundEqualSign) {
            var spaceIndex = attrValue.indexOf(" ");
            attrValue = attrValue.substring(0, (spaceIndex > offset) ? spaceIndex : offset);
        } else if (offset > 0 && (startChar === "'" || startChar === '"')) {
            //The att value is getting edit in progress. There is possible extra
            //stuff in this token state since the quote isn't closed, so we assume
            //the stuff from the quote to the current pos is definitely in the attribute
            //value.
            attrValue = attrValue.substring(0, offset);
        }

        //If the attrValue start with a quote, trim that now
        startChar = attrValue.charAt(0);
        if (startChar === "'" || startChar === '"') {
            attrValue = attrValue.substring(1);
            offset--;
        } else {
            startChar = "";
        }

        return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: false};
    }

    /**
     * @private
     * Gets the tagname from where ever you are in the currect state
     * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context
     * @return {string}
     */
    function _extractTagName(ctx) {
        if (ctx.token.state.tagName) {
            return ctx.token.state.tagName;           //XML mode
        } else if (ctx.token.state.htmlState) {
            return ctx.token.state.htmlState.tagName; //HTML mode
        } else if (ctx.token.state.html) {
            return ctx.token.state.html.tagName;      //HTML mode for PHP files
        }
        // Some mixed modes that offer HTML as a nested mode don't actually expose the HTML state
        return null;
    }

    /**
     * Compiles a list of used attributes for a given tag
     * @param {CodeMirror} editor An instance of a CodeMirror editor
     * @param {ch:{string}, line:{number}} pos A CodeMirror position
     * @return {Array.<string>} A list of the used attributes inside the current tag
     */
    function getTagAttributes(editor, pos) {
        var attrs       = [],
            backwardCtx = TokenUtils.getInitialContext(editor._codeMirror, pos),
            forwardCtx  = $.extend({}, backwardCtx);

        if (editor.getModeForSelection() === "html") {
            if (backwardCtx.token && backwardCtx.token.className !== "tag") {
                while (TokenUtils.movePrevToken(backwardCtx) && backwardCtx.token.className !== "tag") {
                    if (backwardCtx.token.className === "error" && backwardCtx.token.string.indexOf("<") === 0) {
                        break;
                    }
                    if (backwardCtx.token.className === "attribute") {
                        attrs.push(backwardCtx.token.string);
                    }
                }

                while (TokenUtils.moveNextToken(forwardCtx) && forwardCtx.token.className !== "tag") {
                    if (forwardCtx.token.className === "attribute") {
                        // If the current tag is not closed, codemirror may return the next opening
                        // tag as an attribute. Stop the search loop in that case.
                        if (forwardCtx.token.string.indexOf("<") === 0) {
                            break;
                        }
                        attrs.push(forwardCtx.token.string);
                    } else if (forwardCtx.token.className === "error") {
                        if (forwardCtx.token.string.indexOf("<") === 0 || forwardCtx.token.string.indexOf(">") === 0) {
                            break;
                        }
                        // If we type the first letter of the next attribute, it comes as an error
                        // token. We need to double check for possible invalidated attributes.
                        if (forwardCtx.token.string.trim() !== "" &&
                                forwardCtx.token.string.indexOf("\"") === -1 &&
                                forwardCtx.token.string.indexOf("'") === -1 &&
                                forwardCtx.token.string.indexOf("=") === -1) {
                            attrs.push(forwardCtx.token.string);
                        }
                    }
                }
            }
        }

        return attrs;
    }

    /**
     * Creates a tagInfo object and assures all the values are entered or are empty strings
     * @param {string=} tokenType what is getting edited and should be hinted
     * @param {number=} offset where the cursor is for the part getting hinted
     * @param {string=} tagName The name of the tag
     * @param {string=} attrName The name of the attribute
     * @param {string=} attrValue The value of the attribute
     * @return {{tagName:string,
     *           attr:{name:string, value:string, valueAssigned:boolean, quoteChar:string, hasEndQuote:boolean},
     *           position:{tokenType:string, offset:number}
     *         }}
     *         A tagInfo object with some context about the current tag hint.
     */
    function createTagInfo(tokenType, offset, tagName, attrName, attrValue, valueAssigned, quoteChar, hasEndQuote) {
        return { tagName: tagName || "",
                 attr:
                    { name: attrName || "",
                      value: attrValue || "",
                      valueAssigned: valueAssigned || false,
                      quoteChar: quoteChar || "",
                      hasEndQuote: hasEndQuote || false },
                 position:
                    { tokenType: tokenType || "",
                      offset: offset || 0 } };
    }

    /**
     * @private
     * Gets the taginfo starting from the attribute value and moving backwards
     * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context
     * @return {string}
     */
    function _getTagInfoStartingFromAttrValue(ctx) {
        // Assume we in the attr value
        // and validate that by going backwards
        var attrInfo = _extractAttrVal(ctx),
            attrVal = attrInfo.val,
            offset = attrInfo.offset,
            quoteChar = attrInfo.quoteChar,
            hasEndQuote = attrInfo.hasEndQuote,
            strLength = ctx.token.string.length;

        if (ctx.token.className === "string" && ctx.pos.ch === ctx.token.end && strLength > 1) {
            var firstChar = ctx.token.string[0],
                lastChar = ctx.token.string[strLength - 1];

            // We get here only when the cursor is immediately on the right of the end quote
            // of an attribute value. So we want to return an empty tag info so that the caller
            // can dismiss the code hint popup if it is still open.
            if (firstChar === lastChar && (firstChar === "'" || firstChar === "\"")) {
                return createTagInfo();
            }
        }

        //Move to the prev token, and check if it's "="
        if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.string !== "=") {
            return createTagInfo();
        }

        //Move to the prev token, and check if it's an attribute
        if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.className !== "attribute") {
            return createTagInfo();
        }

        var attrName = ctx.token.string;
        var tagName = _extractTagName(ctx);

        //We're good.
        return createTagInfo(ATTR_VALUE, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote);
    }

    /**
     * @private
     * Gets the taginfo starting from the attribute name and moving forwards
     * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context
     * @param {boolean} isPriorAttr indicates whether we're getting info for a prior attribute
     * @return {string}
     */
    function _getTagInfoStartingFromAttrName(ctx, isPriorAttr) {
        //Verify We're in the attribute name, move forward and try to extract the rest of
        //the info. If the user it typing the attr the rest might not be here
        if (isPriorAttr === false && ctx.token.className !== "attribute") {
            return createTagInfo();
        }

        var tagName = _extractTagName(ctx);
        var attrName = ctx.token.string;
        var offset = TokenUtils.offsetInToken(ctx);

        if (!TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) || ctx.token.string !== "=") {
            // If we're checking for a prior attribute and the next token we get is a tag or an html comment or
            // an undefined token class, then we've already scanned past our original cursor location.
            // So just return an empty tag info.
            if (isPriorAttr &&
                    (!ctx.token.className ||
                    (ctx.token.className !== "attribute" && ctx.token.string.indexOf("<") !== -1))) {
                return createTagInfo();
            }
            return createTagInfo(ATTR_NAME, offset, tagName, attrName);
        }

        if (!TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx)) {
            return createTagInfo(ATTR_NAME, offset, tagName, attrName);
        }
        //this should be the attrvalue
        var attrInfo = _extractAttrVal(ctx),
            attrVal = attrInfo.val,
            quoteChar = attrInfo.quoteChar,
            hasEndQuote = attrInfo.hasEndQuote;

        return createTagInfo(ATTR_NAME, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote);
    }

    /**
     * Figure out if we're in a tag, and if we are return info about it
     * An example token stream for this tag is  :
     *      className:tag       string:""
     * @param {Editor} editor An instance of a Brackets editor
     * @param {{ch: number, line: number}} constPos  A CM pos (likely from editor.getCursor())
     * @return {{tagName:string,
     *           attr:{name:string, value:string, valueAssigned:boolean, quoteChar:string, hasEndQuote:boolean},
     *           position:{tokenType:string, offset:number}
     *         }}
     *         A tagInfo object with some context about the current tag hint.
     */
    function getTagInfo(editor, constPos) {
        // We're going to be changing pos a lot, but we don't want to mess up
        // the pos the caller passed in so we use extend to make a safe copy of it.
        var pos = $.extend({}, constPos),
            ctx = TokenUtils.getInitialContext(editor._codeMirror, pos),
            tempCtx = null,
            offset = TokenUtils.offsetInToken(ctx),
            tagInfo,
            tokenType;

        // check if this is inside a style block.
        if (editor.getModeForSelection() !== "html") {
            return createTagInfo();
        }

        //check and see where we are in the tag
        if (ctx.token.string.length > 0 && ctx.token.string.trim().length === 0) {

            // token at (i.e. before) pos is whitespace, so test token at next pos
            //
            // note: getTokenAt() does range checking for ch. If it detects that ch is past
            // EOL, it uses EOL, same token is returned, and the following condition fails,
            // so we don't need to worry about testPos being valid.
            var testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line},
                testToken = editor._codeMirror.getTokenAt(testPos);

            if (testToken.string.length > 0 && testToken.string.trim().length > 0 &&
                    testToken.string.charAt(0) !== ">") {
                // pos has whitespace before it and non-whitespace after it, so use token after
                ctx.token = testToken;

                if (ctx.token.className === "tag" || ctx.token.className === "error") {
                    // Check to see if the cursor is just before a "<" but not in any tag.
                    if (ctx.token.string.charAt(0) === "<") {
                        return createTagInfo();
                    }
                } else if (ctx.token.className === "attribute") {
                    // Check to see if the user is going to add a new attr before an existing one
                    return _getTagInfoStartingFromAttrName(ctx, false);
                } else if (ctx.token.string === "=") {
                    // We're between a whitespace and  "=", so return an empty tag info.
                    return createTagInfo();
                }
            } else {
                // We get here if ">" or white spaces after testPos.
                // Check if there is an equal sign after testPos by creating a new ctx
                // with the original pos. We can't use the current ctx since we need to
                // use it to scan backwards if we don't find an equal sign here.
                // Comment out this block to fix issue #1510.
//                if (testToken.string.length > 0 && testToken.string.charAt(0) !== ">") {
//                    tempCtx = TokenUtils.getInitialContext(editor._codeMirror, pos);
//                    if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, tempCtx) && tempCtx.token.string === "=") {
//                        // Return an empty tag info since we're between an atribute name and the equal sign.
//                        return createTagInfo();
//                    }
//                }

                // next, see what's before pos
                if (!TokenUtils.movePrevToken(ctx)) {
                    return createTagInfo();
                }

                if (ctx.token.className === "comment") {
                    return createTagInfo();
                } else if (ctx.token.className !== "tag" && ctx.token.string !== "=") {
                    // If it wasn't the tag name, assume it was an attr value
                    // Also we don't handle the "=" here.
                    tagInfo = _getTagInfoStartingFromAttrValue(ctx);

                    // Check to see if this is the closing of a tag (either the start or end)
                    // or a comment tag.
                    if (ctx.token.className === "comment" ||
                            (ctx.token.className === "tag" &&
                            (ctx.token.string === ">" || ctx.token.string === "/>" ||
                                (ctx.token.string.charAt(0) === "<" && ctx.token.string.charAt(1) === "/")))) {
                        return createTagInfo();
                    }

                    // If it wasn't an attr value, assume it was an empty attr (ie. attr with no value)
                    if (!tagInfo.tagName) {
                        tagInfo = _getTagInfoStartingFromAttrName(ctx, true);
                    }

                    // We don't want to give context for the previous attr
                    // and we want it to look like the user is going to add a new attr
                    if (tagInfo.tagName) {
                        return createTagInfo(ATTR_NAME, 0, tagInfo.tagName);
                    }
                    return createTagInfo();
                }

                // We know the tag was here, so the user is adding an attr name
                tokenType = ATTR_NAME;
                offset = 0;
            }
        }

        if (ctx.token.className === "tag" || ctx.token.className === "error") {
            // Check if the user just typed a white space after "<" that made an existing tag invalid.
            if (ctx.token.string.match(/^<\s+/) && offset !== 1) {
                return createTagInfo();
            }

            // Check to see if this is the closing of a tag (either the start or end)
            if (ctx.token.string === ">" || ctx.token.string === "/>" ||
                    (ctx.token.string.charAt(0) === "<" && ctx.token.string.charAt(1) === "/")) {
                return createTagInfo();
            }

            if (!tokenType) {
                tokenType = TAG_NAME;
                offset--; //need to take off 1 for the leading "<"
            }

            // We're actually in the tag, just return that as we have no relevant
            // info about what attr is selected
            return createTagInfo(tokenType, offset, _extractTagName(ctx));
        }

        if (ctx.token.string === "=") {
            // We could be between the attr and the value
            // Step back and check
            if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.className !== "attribute") {
                return createTagInfo();
            }

            // The "=" is added, time to hint for values
            tokenType = ATTR_VALUE;
            offset = 0;
        }

        if (ctx.token.className === "attribute") {
            tagInfo = _getTagInfoStartingFromAttrName(ctx, false);

            // If we're in attr value, then we may need to calculate the correct offset
            // from the beginning of the attribute value. If the cursor position is to
            // the left of attr value, then the offset is negative.
            // e.g. if the cursor is just to the right of the "=" in <a rel= "rtl", then
            // the offset will be -2.
            if (tagInfo.attr.quoteChar) {
                offset = constPos.ch - ctx.pos.ch;
            } else if (tokenType === ATTR_VALUE && (constPos.ch + 1) < ctx.pos.ch) {
                // The cursor may be right before an unquoted attribute or another attribute name.
                // Since we can't distinguish between them, we will empty the value so that the
                // caller can just insert a new attribute value.
                tagInfo.attr.value = "";
            }
        } else {
            // if we're not at a tag, "=", or attribute name, assume we're in the value
            tagInfo = _getTagInfoStartingFromAttrValue(ctx);
        }

        if (tokenType && tagInfo.tagName) {
            tagInfo.position.tokenType = tokenType;
            tagInfo.position.offset = offset;
        }

        return tagInfo;
    }

    /**
     * Returns an Array of info about all


/*!
 * jQuery UI CSS Framework @VERSION
 *
 * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Theming/API
 *
 * To view and modify this theme, visit http://jqueryui.com/themeroller/
 */

/* Component containers
----------------------------------*/
.ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; }
.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(images/ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; }
.ui-widget-content a { color: #222222/*{fcContent}*/; }
.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; }
.ui-widget-header a { color: #222222/*{fcHeader}*/; }

/* Interaction states
----------------------------------*/
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; }
.ui-state-hover a, .ui-state-hover a:hover { color: #212121/*{fcHover}*/; text-decoration: none; }
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; }
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; }
.ui-widget :active { outline: none; }

/* Interaction Cues
----------------------------------*/
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; }
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; }
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; }
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; }
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; }
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }

/* Icons
----------------------------------*/

/* states and images */
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; }
.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; }
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; }
.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; }
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; }
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; }

/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }

/* Misc visuals
----------------------------------*/

/* Corner radius */
.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -khtml-border-top-left-radius: 4px/*{cornerRadius}*/; border-top-left-radius: 4px/*{cornerRadius}*/; }
.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -khtml-border-top-right-radius: 4px/*{cornerRadius}*/; border-top-right-radius: 4px/*{cornerRadius}*/; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-left-radius: 4px/*{cornerRadius}*/; border-bottom-left-radius: 4px/*{cornerRadius}*/; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; }

/* Overlays */
.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; }
.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRadiusShadow}*/; border-radius: 8px/*{cornerRadiusShadow}*/; }


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, PathUtils, window, Mustache */

/**
 *  Utilities functions for displaying update notifications
 *
 */
define(function (require, exports, module) {
    "use strict";

    var Dialogs              = require("widgets/Dialogs"),
        NativeApp            = require("utils/NativeApp"),
        PreferencesManager   = require("preferences/PreferencesManager"),
        Strings              = require("strings"),
        StringUtils          = require("utils/StringUtils"),
        Global               = require("utils/Global"),
        UpdateDialogTemplate = require("text!htmlContent/update-dialog.html");

    // Extract current build number from package.json version field 0.0.0-0
    var _buildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]);

    // PreferenceStorage
    var _prefs = PreferencesManager.getPreferenceStorage(module.id, {lastNotifiedBuildNumber: 0});

    // This is the last version we notified the user about. If checkForUpdate()
    // is called with "false", only show the update notification dialog if there
    // is an update newer than this one. This value is saved in preferences.
    var _lastNotifiedBuildNumber = _prefs.getValue("lastNotifiedBuildNumber");

    // Last time the versionInfoURL was fetched
    var _lastInfoURLFetchTime = _prefs.getValue("lastInfoURLFetchTime");

    // URL to load version info from. By default this is loaded no more than once a day. If
    // you force an update check it is always loaded.

    // URL to fetch the version information.
    var _versionInfoURL;

    // Information on all posted builds of Brackets. This is an Array, where each element is
    // an Object with the following fields:
    //
    //  {Number} buildNumber Number of the build
    //  {String} versionString String representation of the build number (ie "Sprint 14")
    //  {String} dateString Date of the build
    //  {String} releaseNotesURL URL of the release notes for this build
    //  {String} downloadURL URL to download this build
    //  {Array} newFeatures Array of new features in this build. Each entry has two fields:
    //      {String} name Name of the feature
    //      {String} description Description of the feature
    //
    // This array must be reverse sorted by buildNumber (newest build info first)

    /**
     * @private
     * Flag that indicates if we've added a click handler to the update notification icon.
     */
    var _addedClickHandler = false;

    /**
     * Get a data structure that has information for all builds of Brackets.
     *
     * If force is true, the information is always fetched from _versionInfoURL.
     * If force is false, we try to use cached information. If more than
     * 24 hours have passed since the last fetch, or if cached data can't be found,
     * the data is fetched again.
     *
     * If new data is fetched and dontCache is false, the data is saved in preferences
     * for quick fetching later.
     */
    function _getUpdateInformation(force, dontCache) {
        var result = new $.Deferred();
        var fetchData = false;
        var data;

        // If force is true, always fetch
        if (force) {
            fetchData = true;
        }

        // If we don't have data saved in prefs, fetch
        data = _prefs.getValue("updateInfo");
        if (!data) {
            fetchData = true;
        }

        // If more than 24 hours have passed since our last fetch, fetch again
        if ((new Date()).getTime() > _lastInfoURLFetchTime + (1000 * 60 * 60 * 24)) {
            fetchData = true;
        }

        if (fetchData) {
            $.ajax(_versionInfoURL, {
                dataType: "text",
                cache: false,
                complete: function (jqXHR, status) {
                    if (status === "success") {
                        try {
                            data = JSON.parse(jqXHR.responseText);
                            if (!dontCache) {
                                _lastInfoURLFetchTime = (new Date()).getTime();
                                _prefs.setValue("lastInfoURLFetchTime", _lastInfoURLFetchTime);
                                _prefs.setValue("updateInfo", data);
                            }
                            result.resolve(data);
                        } catch (e) {
                            console.log("Error parsing version information");
                            console.log(e);
                            result.reject();
                        }
                    }
                },
                error: function (jqXHR, status, error) {
                    // When loading data for unit tests, the error handler is
                    // called but the responseText is valid. Try to use it here,
                    // but *don't* save the results in prefs.

                    if (!jqXHR.responseText) {
                        // Text is NULL or empty string, reject().
                        result.reject();
                        return;
                    }

                    try {
                        data = JSON.parse(jqXHR.responseText);
                        result.resolve(data);
                    } catch (e) {
                        result.reject();
                    }
                }
            });
        } else {
            result.resolve(data);
        }

        return result.promise();
    }

    /**
     * Return a new array of version information that is newer than "buildNumber".
     * Returns null if there is no new version information.
     */
    function _stripOldVersionInfo(versionInfo, buildNumber) {
        // Do a simple linear search. Since we are going in reverse-chronological order, we
        // should get through the search quickly.
        var lastIndex = 0;
        var len = versionInfo.length;

        while (lastIndex < len) {
            if (versionInfo[lastIndex].buildNumber <= buildNumber) {
                break;
            }
            lastIndex++;
        }

        if (lastIndex > 0) {
            return versionInfo.slice(0, lastIndex);
        }

        // No new version info
        return null;
    }

    /**
     * Show a dialog that shows the update
     */
    function _showUpdateNotificationDialog(updates) {
        Dialogs.showModalDialogUsingTemplate(Mustache.render(UpdateDialogTemplate, Strings))
            .done(function (id) {
                if (id === Dialogs.DIALOG_BTN_DOWNLOAD) {
                    // The first entry in the updates array has the latest download link
                    NativeApp.openURLInDefaultBrowser(updates[0].downloadURL);
                }
            });

        // Populate the update data
        var $dlg = $(".update-dialog.instance");
        var $updateList = $dlg.find(".update-info");

        // TODO: Use a template instead of hand-rolling HTML code
        updates.forEach(function (item, index) {
            var $features = $("
");

            item.newFeatures.forEach(function (feature, index) {
                $features.append(
                    "
" +
                        StringUtils.htmlEscape(feature.name) +
                        " - " +
                        StringUtils.htmlEscape(feature.description) +
                        "
"
                );
            });

            var $item = $("
")
                .append("
" +
                        StringUtils.htmlEscape(item.versionString) +
                        " - " +
                        StringUtils.htmlEscape(item.dateString) +
                        " (" +
                        Strings.RELEASE_NOTES +
                        ")
")
                .append($features)
                .appendTo($updateList);
        });

        $dlg.on("click", "a", function (e) {
            var url = $(e.target).attr("data-url");

            if (url) {
                // Make sure the URL has a domain that we know about
                if (/(brackets\.io|github\.com|adobe\.com)$/i.test(PathUtils.parseUrl(url).hostname)) {
                    NativeApp.openURLInDefaultBrowser(url);
                }
            }
        });
    }

    /**
     * Check for updates. If "force" is true, update notification dialogs are always displayed
     * (if an update is available). If "force" is false, the update notification is only
     * displayed for newly available updates.
     *
     * If an update is available, show the "update available" notification icon in the title bar.
     *
     * @param {boolean} force If true, always show the notification dialog.
     * @param {Object} _testValues This should only be used for testing purposes. See comments for details.
     * @return {$.Promise} jQuery Promise object that is resolved or rejected after the update check is complete.
     */
    function checkForUpdate(force, _testValues) {
        // The second param, if non-null, is an Object containing value overrides. Values
        // in the object temporarily override the local values. This should *only* be used for testing.
        // If any overrides are set, permanent changes are not made (including showing
        // the update notification icon and saving prefs).
        var oldValues;
        var usingOverrides = false; // true if any of the values are overridden.
        var result = new $.Deferred();

        if (_testValues) {
            oldValues = {};

            if (_testValues.hasOwnProperty("_buildNumber")) {
                oldValues._buildNumber = _buildNumber;
                _buildNumber = _testValues._buildNumber;
                usingOverrides = true;
            }

            if (_testValues.hasOwnProperty("_lastNotifiedBuildNumber")) {
                oldValues._lastNotifiedBuildNumber = _lastNotifiedBuildNumber;
                _lastNotifiedBuildNumber = _testValues._lastNotifiedBuildNumber;
                usingOverrides = true;
            }

            if (_testValues.hasOwnProperty("_versionInfoURL")) {
                oldValues._versionInfoURL = _versionInfoURL;
                _versionInfoURL = _testValues._versionInfoURL;
                usingOverrides = true;
            }
        }

        _getUpdateInformation(force || usingOverrides, usingOverrides)
            .done(function (versionInfo) {
                // Get all available updates
                var allUpdates = _stripOldVersionInfo(versionInfo, _buildNumber);

                // When running directly from GitHub source (as opposed to
                // an installed build), _buildNumber is 0. In this case, if the
                // test is not forced, don't show the update notification icon or
                // dialog.
                if (_buildNumber === 0 && !force) {
                    result.resolve();
                    return;
                }

                if (allUpdates) {
                    // Always show the "update available" icon if any updates are available
                    var $updateNotification = $("#update-notification");

                    $updateNotification.css("display", "inline-block");
                    if (!_addedClickHandler) {
                        _addedClickHandler = true;
                        $updateNotification.on("click", function () {
                            checkForUpdate(true);
                        });
                    }

                    // Only show the update dialog if force = true, or if the user hasn't been
                    // alerted of this update
                    if (force || allUpdates[0].buildNumber >  _lastNotifiedBuildNumber) {
                        _showUpdateNotificationDialog(allUpdates);

                        // Update prefs with the last notified build number
                        _lastNotifiedBuildNumber = allUpdates[0].buildNumber;
                        // Don't save prefs is we have overridden values
                        if (!usingOverrides) {
                            _prefs.setValue("lastNotifiedBuildNumber", _lastNotifiedBuildNumber);
                        }
                    }
                } else if (force) {
                    // No updates are available. If force == true, let the user know.
                    Dialogs.showModalDialog(
                        Dialogs.DIALOG_ID_ERROR,
                        Strings.NO_UPDATE_TITLE,
                        Strings.NO_UPDATE_MESSAGE
                    );
                }

                if (oldValues) {
                    if (oldValues.hasOwnProperty("_buildNumber")) {
                        _buildNumber = oldValues._buildNumber;
                    }
                    if (oldValues.hasOwnProperty("_lastNotifiedBuildNumber")) {
                        _lastNotifiedBuildNumber = oldValues._lastNotifiedBuildNumber;
                    }
                    if (oldValues.hasOwnProperty("_versionInfoURL")) {
                        _versionInfoURL = oldValues._versionInfoURL;
                    }
                }
                result.resolve();
            })
            .fail(function () {
                // Error fetching the update data. If this is a forced check, alert the user
                if (force) {
                    Dialogs.showModalDialog(
                        Dialogs.DIALOG_ID_ERROR,
                        Strings.ERROR_FETCHING_UPDATE_INFO_TITLE,
                        Strings.ERROR_FETCHING_UPDATE_INFO_MSG
                    );
                }
                result.reject();
            });

        return result.promise();
    }

    // Append locale to version info URL
    _versionInfoURL = brackets.config.update_info_url + brackets.getLocale() + ".json";

    // Define public API
    exports.checkForUpdate = checkForUpdate;
});


<!doctype html>

	<meta charset="utf-8">
	<title>Position Visual Test: Feedback</title>
	<link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css">

all around me


import VNode from './vnode';
import { optionsHook } from './hooks';
import { empty } from './util';
const SHARED_TEMP_ARRAY = [];

keep keep replace keep keep keep keep keep

import { NON_DIMENSION_PROPS } from './constants';

/** Copy own-properties from `props` onto `obj`.
 *	@returns obj
 *	@private
 */
export function extend(obj, props) {
	for (let i in props) if (hasOwnProperty.call(props, i)) {
		obj[i] = props[i];
	}
	return obj;
}

/** Fast clone. Note: does not filter out non-own properties. */
export function clone(obj) {
	let out = {};
	/*eslint guard-for-in:0*/
	for (let i in obj) out[i] = obj[i];
	return out;
}

/** Create a caching wrapper for the given function.
 *	@private
 */
export function memoize(fn, mem) {
	mem = mem || {};
	return k => hasOwnProperty.call(mem, k) ? mem[k] : (mem[k] = fn(k));
}

/** Get a deep property value from the given object, expressed in dot-notation.
 *	@private
 */
export function delve(obj, key) {
	for (let p=key.split('.'), i=0; i<p.length && obj; i++) {
		obj = obj[p];
	}
	return obj;
}

/** Convert an Array-like object to an Array
 *	@private
 */
export function toArray(obj) {
	let arr = [],
		i = obj.length;
	while (i--) arr[i] = obj[i];
	return arr;
}

/** @private is the given object a Function? */
export const isFunction = obj => 'function'===typeof obj;

/** @private is the given object a String? */
export const isString = obj => 'string'===typeof obj;

/** @private Safe reference to builtin hasOwnProperty */
export const hasOwnProperty = {}.hasOwnProperty;

/** Check if a value is `null` or `undefined`.
 *	@private
 */
export const empty = x => x==null;

/** Convert a hashmap of styles to CSSText
 *	@private
 */
export function styleObjToCss(s) {
	let str = '';
	for (let prop in s) {
		if (hasOwnProperty.call(s, prop)) {
			let val = s[prop];
			if (!empty(val)) {
				str += jsToCss(prop);
				str += ': ';
				str += val;
				if (typeof val==='number' && !NON_DIMENSION_PROPS[prop]) {
					str += 'px';
				}
				str += '; ';
			}
		}
	}
	return str;
}

/** Convert a hashmap of CSS classes to a space-delimited className string
 *	@private
 */
export function hashToClassName(c) {
	let str = '';
	for (let prop in c) {
		if (c[prop]) {
			if (str) str += ' ';
			str += prop;
		}
	}
	return str;
}

/** Convert a JavaScript camel-case CSS property name to a CSS property name
 *	@private
 *	@function
 */
export const jsToCss = memoize( s => s.replace(/([A-Z])/g,'-$1').toLowerCase() );

/** Just a memoized String.prototype.toLowerCase */
export const toLowerCase = memoize( s => s.toLowerCase() );

// For animations, rAF is vastly superior. However, it scores poorly on benchmarks :(
// export const setImmediate = typeof requestAnimationFrame==='function' ? requestAnimationFrame : setTimeout;

let ch;
try { ch = new MessageChannel(); } catch (e) {}

/** Call a function asynchronously, as soon as possible.
 *	@param {Function} callback
 */
export const setImmediate = ch ? ( f => {
	ch.port1.onmessage = f;
	ch.port2.postMessage('');
}) : setTimeout;


preact

Tiny & fast Component-based virtual DOM framework.

rollup -c rollup.config.js -m ${npm_package_main}.map -f umd -n $npm_package_amdName src/preact.js -o $npm_package_main

uglifyjs $npm_package_main -c unsafe,keep_fargs=false,pure_getters,screw_ie8 -m -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map

size=$(gzip-size $npm_package_minified_main) && echo "gzip size: $size / $(pretty-bytes $size)"

npm-run-all lint --parallel test:mocha test:karma

mocha --recursive --compilers js:babel/register test/shared test/node

npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish


import { h } from '../../src/preact';
import VNode from '../../src/vnode';
import { expect } from 'chai';

/*eslint-env browser, mocha */

/** @jsx h */

let flatten = obj => JSON.parse(JSON.stringify(obj));

describe('h(jsx)', () => {
	it('should return a VNode', () => {
		let r;
		expect( () => r = h('foo') ).not.to.throw();
		expect(r).to.be.an('object');
		expect(r).to.be.an.instanceof(VNode);
		expect(r).to.have.property('nodeName', 'foo');
		expect(r).to.have.property('attributes', undefined);
		expect(r).to.have.property('children', undefined);
	});

	it('should perserve raw attributes', () => {
		let attrs = { foo:'bar', baz:10, func:()=>{} },
			r = h('foo', attrs);
		expect(r).to.be.an('object')
			.with.property('attributes')
			.that.deep.equals(attrs);
	});

	it('should support element children', () => {
		let r = h(
			'foo',
			null,
			h('bar'),
			h('baz')
		);

		expect(r).to.be.an('object')
			.with.property('children')
			.that.deep.equals([
				new VNode('bar'),
				new VNode('baz')
			]);
	});

	it('should support element children', () => {
		let r = h(
			'foo',
			null,
			h('bar'),
			h('baz', null, h('test'))
		);

		r = flatten(r);

		expect(r).to.be.an('object')
			.with.property('children')
			.that.deep.equals([
				{ nodeName:'bar' },
				{ nodeName:'baz', children:[
					{ nodeName:'test' }
				]}
			]);
	});

	it('should support text children', () => {
		let r = h(
			'foo',
			null,
			'textstuff'
		);

		expect(r).to.be.an('object')
			.with.property('children')
			.with.length(1)
			.with.property('0')
			.that.equals('textstuff');
	});

	it('should merge adjacent text children', () => {
		let r = h(
			'foo',
			null,
			'one',
			'two',
			h('bar'),
			'three',
			h('baz'),
			h('baz'),
			'four',
			'five',
			'six'
		);

		r = flatten(r);

		expect(r).to.be.an('object')
			.with.property('children')
			.that.deep.equals([
				'onetwo',
				{ nodeName:'bar' },
				'three',
				{ nodeName:'baz' },
				{ nodeName:'baz' },
				'fourfivesix'
			]);
	});
});


import { empty, isString, isFunction, delve } from './util';

/** Create an Event handler function that sets a given state property.
 *	@param {Component} component	The component whose state should be updated
 *	@param {string} key				A dot-notated key path to update in the component's state
 *	@param {string} eventPath		A dot-notated key path to the value that should be retrieved from the Event or component
 *	@returns {function} linkedStateHandler
 *	@private
 */
export function createLinkedState(component, key, eventPath) {
	let path = key.split('.'),
		p0 = path[0],
		len = path.length;
	return function(e) {
		let t = this,
			s = component.state,
			obj = s,
			v, i;
		if (isString(eventPath)) {
			v = delve(e, eventPath);
			if (empty(v) && (t=t._component)) {
				v = delve(t, eventPath);
			}
		}
		else {
			v = (t.nodeName+t.type).match(/^input(checkbox|radio)$/i) ? t.checked : t.value;
		}
		if (isFunction(v)) v = v.call(t);
		if (len>1) {
			for (i=0; i<len-1; i++) {
				obj = obj[path[i]] || (obj[path[i]] = {});
			}
			obj[path[i]] = v;
			v = s[p0];
		}
		component.setState({ [p0]: v });
	};
}


keep keep keep replace keep keep keep keep

import VNode from './vnode';
import { optionsHook } from './hooks';
import { empty } from './util';

const SHARED_TEMP_ARRAY = [];

/** JSX/hyperscript reviver
 *	@see http://jasonformat.com/wtf-is-jsx
 *	@public
 *  @example
 *  /** @jsx h *\/
 *  import { render, h } from 'preact';
 *  render(foo, document.body);
 */
export default function h(nodeName, attributes) {
	let len = arguments.length,
		children, arr, lastSimple;

	if (len>2) {
		children = [];
		for (let i=2; i<len; i++) {
			let p = arguments[i];
			if (empty(p)) continue;
			if (p.join) {
				arr = p;
			}
			else {
				arr = SHARED_TEMP_ARRAY;
				arr[0] = p;
			}
			for (let j=0; j<arr.length; j++) {
				let child = arr[j],
					simple = !empty(child) && !(child instanceof VNode);
				if (simple) child = String(child);
				if (simple && lastSimple) {
					children[children.length-1] += child;
				}
				else if (!empty(child)) {
					children.push(child);
				}
				lastSimple = simple;
			}
		}
	}

	if (attributes && attributes.children) {
		delete attributes.children;
	}

	let p = new VNode(nodeName, attributes || undefined, children || undefined);
	optionsHook('vnode', p);
	return p;
}


import { h, render, Component } from '../../src/preact';
/** @jsx h */

describe('context', () => {
	let scratch;

	before( () => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});

	beforeEach( () => {
		scratch.innerHTML = '';
	});

	after( () => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});

	it('should pass context to grandchildren', () => {
		const CONTEXT = { a:'a' };
		const PROPS = { b:'b' };

		class Outer extends Component {
			getChildContext() {
				return CONTEXT;
			}
			render(props) {
				return
<Inner {...props} />
;
			}
		}
		sinon.spy(Outer.prototype, 'getChildContext');

		class Inner extends Component {
			shouldComponentUpdate() { return true; }
			componentWillReceiveProps() {}
			componentWillUpdate() {}
			componentDidUpdate() {}
			render(props, state, context) {
				return
{ context && context.a }
;
			}
		}
		sinon.spy(Inner.prototype, 'shouldComponentUpdate');
		sinon.spy(Inner.prototype, 'componentWillReceiveProps');
		sinon.spy(Inner.prototype, 'componentWillUpdate');
		sinon.spy(Inner.prototype, 'componentDidUpdate');
		sinon.spy(Inner.prototype, 'render');

		render(<Outer />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledOnce;

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT);

		CONTEXT.foo = 'bar';
		render(<Outer {...PROPS} />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledTwice;

		expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT);
		expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT);
		expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {});
		expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {});
		expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT);
	});

	it('should pass context to direct children', () => {
		const CONTEXT = { a:'a' };
		const PROPS = { b:'b' };

		class Outer extends Component {
			getChildContext() {
				return CONTEXT;
			}
			render(props) {
				return <Inner {...props} />;
			}
		}
		sinon.spy(Outer.prototype, 'getChildContext');

		class Inner extends Component {
			shouldComponentUpdate() { return true; }
			componentWillReceiveProps() {}
			componentWillUpdate() {}
			componentDidUpdate() {}
			render(props, state, context) {
				return
{ context && context.a }
;
			}
		}
		sinon.spy(Inner.prototype, 'shouldComponentUpdate');
		sinon.spy(Inner.prototype, 'componentWillReceiveProps');
		sinon.spy(Inner.prototype, 'componentWillUpdate');
		sinon.spy(Inner.prototype, 'componentDidUpdate');
		sinon.spy(Inner.prototype, 'render');

		render(<Outer />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledOnce;

		// initial render does not invoke anything but render():
		expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT);

		CONTEXT.foo = 'bar';
		render(<Outer {...PROPS} />, scratch, scratch.lastChild);

		expect(Outer.prototype.getChildContext).to.have.been.calledTwice;

		expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT);
		expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT);
		expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {});
		expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {});
		expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT);

		// make sure render() could make use of context.a
		expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] }));
	});
});


/node_modules
/npm-debug.log
.DS_Store
/dist
/_dev


simple = !empty(child) && !(child instanceof VNode);

keep keep keep keep keep replace keep keep keep keep

simple = !falsey(child) && !(child instanceof VNode);


import diff from './vdom/diff';
import { deepHook } from './hooks';

/** Render JSX into a `parent` Element.
 *	@param {VNode} vnode		A (JSX) VNode to render
 *	@param {Element} parent		DOM element to render into
 *	@param {Element} [merge]	Attempt to re-use an existing DOM tree rooted at `merge`
 *	@public
 *
 *	@example
 *	// render a div into :
 *	render(
hello!
, document.body);
 *
 *	@example
 *	// render a "Thing" component into #foo:
 *	const Thing = ({ name }) => { name };
 *	render(<Thing name="one" />, document.querySelector('#foo'));
 */
export default function render(vnode, parent, merge) {
	let existing = merge && merge._component && merge._componentConstructor===vnode.nodeName,
		built = diff(merge, vnode),
		c = !existing && built._component;

	if (c) deepHook(c, 'componentWillMount');

	if (built.parentNode!==parent) {
		parent.appendChild(built);
	}

	if (c) deepHook(c, 'componentDidMount');

	return built;
}


import { h, render, rerender, Component } from '../../src/preact';
/** @jsx h */

describe('Component spec', () => {
	let scratch;

	before( () => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});

	beforeEach( () => {
		scratch.innerHTML = '';
	});

	after( () => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});

	describe('defaultProps', () => {
		it('should apply default props on initial render', () => {
			class WithDefaultProps extends Component {
				constructor(props, context) {
					super(props, context);
					expect(props).to.be.deep.equal({
						fieldA: 1, fieldB: 2,
						fieldC: 1, fieldD: 2
					});
				}
				render() {
					return <div />;
				}
			}
			WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };
			render(<WithDefaultProps fieldA={1} fieldB={2} fieldD={2} />, scratch);
		});

		it('should apply default props on rerender', () => {
			let doRender;
			class Outer extends Component {
				constructor() {
					super();
					this.state = { i:1 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: 2 });
				}
				render(props, { i }) {
					return <WithDefaultProps fieldA={1} fieldB={i} fieldD={i} />;
				}
			}
			class WithDefaultProps extends Component {
				constructor(props, context) {
					super(props, context);
					this.ctor(props, context);
				}
				ctor(){}
				componentWillReceiveProps() {}
				render() {
					return <div />;
				}
			}
			WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };

			let proto = WithDefaultProps.prototype;
			sinon.spy(proto, 'ctor');
			sinon.spy(proto, 'componentWillReceiveProps');
			sinon.spy(proto, 'render');

			render(<Outer />, scratch);
			doRender();

			const PROPS1 = {
				fieldA: 1, fieldB: 1,
				fieldC: 1, fieldD: 1
			};

			const PROPS2 = {
				fieldA: 1, fieldB: 2,
				fieldC: 1, fieldD: 2
			};

			expect(proto.ctor).to.have.been.calledWith(PROPS1);
			expect(proto.render).to.have.been.calledWith(PROPS1);

			rerender();

			// expect(proto.ctor).to.have.been.calledWith(PROPS2);
			expect(proto.componentWillReceiveProps).to.have.been.calledWith(PROPS2);
			expect(proto.render).to.have.been.calledWith(PROPS2);
		});

		// @TODO: migrate this to preact-compat
		xit('should cache default props', () => {
			class WithDefaultProps extends Component {
				constructor(props, context) {
					super(props, context);
					expect(props).to.be.deep.equal({
						fieldA: 1, fieldB: 2,
						fieldC: 1, fieldD: 2,
						fieldX: 10
					});
				}
				getDefaultProps() {
					return { fieldA: 1, fieldB: 1 };
				}
				render() {
					return <div />;
				}
			}
			WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };
			sinon.spy(WithDefaultProps.prototype, 'getDefaultProps');
			render((
				<div>
					<WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
					<WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
					<WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
				</div>
			), scratch);
			expect(WithDefaultProps.prototype.getDefaultProps).to.be.calledOnce;
		});
	});
});


keep keep keep replace keep keep keep keep keep


/** Virtual DOM Node */
export default function VNode(nodeName, attributes, children) {
	/** @type {string|function} */
	this.nodeName = nodeName;

	/** @type {object<string>|undefined} */
	this.attributes = attributes;

	/** @type {array<VNode>|undefined} */
	this.children = children;
}


import h from './h';
import Component from './component';
import render from './render';
import { rerender } from './render-queue';
import options from './options';

export default {
	h,
	Component,
	render,
	rerender,
	options,
	hooks: options
};


/** Convert a hashmap of styles to CSSText

keep keep keep keep add keep keep keep keep keep

/** Check if a value is `null`, `undefined`, or explicitly `false`. */

export const falsey = value => value===false || value==null;


import { hook } from './hooks';
import { extend, clone, isFunction } from './util';
import { createLinkedState } from './linked-state';
import { triggerComponentRender, renderComponent } from './vdom/component';

/** Base Component class, for he ES6 Class method of creating Components
 *	@public
 *
 *	@example
 *	class MyFoo extends Component {
 *		render(props, state) {
 *			return <div />;
 *		}
 *	}
 */
export default function Component(props, context) {
	/** @private */
	this._dirty = this._disableRendering = false;
	/** @private */
	this._linkedStates = {};
	/** @private */
	this._renderCallbacks = [];
	/** @public */
	this.prevState = this.prevProps = this.prevContext = this.base = this._parentComponent = this._component = null;
	/** @public */
	this.context = context || null;
	/** @type {object} */
	this.props = props || {};
	/** @type {object} */
	this.state = hook(this, 'getInitialState') || {};
}

extend(Component.prototype, {

	/** Returns a `boolean` value indicating if the component should re-render when receiving the given `props` and `state`.
	 *	@param {object} nextProps
	 *	@param {object} nextState
	 *	@param {object} nextContext
	 *	@returns {Boolean} should the component re-render
	 *	@name shouldComponentUpdate
	 *	@function
	 */
	// shouldComponentUpdate() {
	// 	return true;
	// },

	/** Returns a function that sets a state property when called.
	 *	Calling linkState() repeatedly with the same arguments returns a cached link function.
	 *
	 *	Provides some built-in special cases:
	 *		- Checkboxes and radio buttons link their boolean `checked` value
	 *		- Inputs automatically link their `value` property
	 *		- Event paths fall back to any associated Component if not found on an element
	 *		- If linked value is a function, will invoke it and use the result
	 *
	 *	@param {string} key				The path to set - can be a dot-notated deep key
	 *	@param {string} [eventPath]		If set, attempts to find the new state value at a given dot-notated path within the object passed to the linkedState setter.
	 *	@returns {function} linkStateSetter(e)
	 *
	 *	@example Update a "text" state value when an input changes:
	 *		<input onChange={ this.linkState('text') } />
	 *
	 *	@example Set a deep state value on click
	 *		<button onClick={ this.linkState('touch.coords', 'touches.0') }>Tap</button
	 */
	linkState(key, eventPath) {
		let c = this._linkedStates,
			cacheKey = key + '|' + (eventPath || '');
		return c[cacheKey] || (c[cacheKey] = createLinkedState(this, key, eventPath));
	},

	/** Update component state by copying properties from `state` to `this.state`.
	 *	@param {object} state		A hash of state properties to update with new values
	 */
	setState(state, callback) {
		let s = this.state;
		if (!this.prevState) this.prevState = clone(s);
		extend(s, isFunction(state) ? state(s, this.props) : state);
		if (callback) this._renderCallbacks.push(callback);
		triggerComponentRender(this);
	},

	/** Immediately perform a synchronous re-render of the component.
	 *	@private
	 */
	forceUpdate() {
		renderComponent(this);
	},

	/** Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
	 *	Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
	 *	@param {object} props		Props (eg: JSX attributes) received from parent element/component
	 *	@param {object} state		The component's current state
	 *	@returns VNode
	 */
	render() {
		return null;
	}

});


import { ATTR_KEY, EMPTY } from '../constants';
import { hasOwnProperty, memoize } from '../util';
import { optionsHook } from '../hooks';

export function ensureNodeData(node) {
	return node[ATTR_KEY] || (node[ATTR_KEY] = {});
}

export function getNodeType(node) {
	return node.nodeType;
}

/** Append multiple children to a Node.
 *	Uses a Document Fragment to batch when appending 2 or more children
 *	@private
 */
export function appendChildren(parent, children) {
	let len = children.length,
		many = len>2,
		into = many ? document.createDocumentFragment() : parent;
	for (let i=0; i<len; i++) into.appendChild(children[i]);
	if (many) parent.appendChild(into);
}

/** Retrieve the value of a rendered attribute
 *	@private
 */
export function getAccessor(node, name, value, cache) {
	if (name!=='type' && name!=='style' && name in node) return node[name];
	let attrs = node[ATTR_KEY];
	if (cache!==false && attrs && hasOwnProperty.call(attrs, name)) return attrs[name];
	if (name==='class') return node.className;
	if (name==='style') return node.style.cssText;
	return value;
}

/** Set a named attribute on the given Node, with special behavior for some names and event handlers.
 *	If `value` is `null`, the attribute/handler will be removed.
 *	@param {Element} node	An element to mutate
 *	@param {string} name	The name/key to set, such as an event or attribute name
 *	@param {any} value		An attribute value, such as a function to be used as an event handler
 *	@param {any} previousValue	The last value that was set for this name/node pair
 *	@private
 */
export function setAccessor(node, name, value) {
	if (name==='class') {
		node.className = value || '';
	}
	else if (name==='style') {
		node.style.cssText = value || '';
	}
	else if (name==='dangerouslySetInnerHTML') {
		node.innerHTML = value.__html;
	}
	else if (name==='key' || (name in node && name!=='type')) {
		node[name] = value;
	}
	else {
		setComplexAccessor(node, name, value);
	}

	ensureNodeData(node)[name] = value;
}

/** For props without explicit behavior, apply to a Node as event handlers or attributes.
 *	@private
 */
function setComplexAccessor(node, name, value) {
	if (name.substring(0,2)==='on') {
		let type = normalizeEventName(name),
			l = node._listeners || (node._listeners = {}),
			fn = !l[type] ? 'add' : !value ? 'remove' : null;
		if (fn) node[fn+'EventListener'](type, eventProxy);
		l[type] = value;
		return;
	}

	let type = typeof value;
	if (value===null) {
		node.removeAttribute(name);
	}
	else if (type!=='function' && type!=='object') {
		node.setAttribute(name, value);
	}
}

/** Proxy an event to hooked event handlers
 *	@private
 */
function eventProxy(e) {
	let fn = this._listeners[normalizeEventName(e.type)];
	if (fn) return fn.call(this, optionsHook('event', e) || e);
}

/** Convert an Event name/type to lowercase and strip any "on*" prefix.
 *	@function
 *	@private
 */
let normalizeEventName = memoize(t => t.replace(/^on/i,'').toLowerCase());

/** Get a hashmap of node properties, preferring preact's cached property values over the DOM's
 *	@private
 */
export function getNodeAttributes(node) {
	return node[ATTR_KEY] || getRawNodeAttributes(node) || EMPTY;
	// let list = getRawNodeAttributes(node),
	// 	l = node[ATTR_KEY];
	// return l && list ? extend(list, l) : (l || list || EMPTY);
}

/** Get a node's attributes as a hashmap, regardless of type.
 *	@private
 */
function getRawNodeAttributes(node) {
	let list = node.attributes;
	if (!list || !list.getNamedItem) return list;
	return getAttributesAsObject(list);
}

/** Convert a DOM `.attributes` NamedNodeMap to a hashmap.
 *	@private
 */
function getAttributesAsObject(list) {
	let attrs;
	for (let i=list.length; i--; ) {
		let item = list[i];
		if (!attrs) attrs = {};
		attrs[item.name] = item.value;
	}
	return attrs;
}


render(<div foo="bar" data-foo="databar" />, scratch);

it('should not render falsey values', () => {

import { h, render, rerender, Component } from '../../src/preact';
/** @jsx h */

describe('render()', () => {
	let scratch;

	before( () => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});

	beforeEach( () => {
		scratch.innerHTML = '';
	});

	after( () => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});

	it('should create empty nodes (<* />)', () => {
		render(
, scratch);
		expect(scratch.childNodes)
			.to.have.length(1)
			.and.to.have.deep.property('0.nodeName', 'DIV');

		scratch.innerHTML = '';

		render(, scratch);
		expect(scratch.childNodes)
			.to.have.length(1)
			.and.to.have.deep.property('0.nodeName', 'SPAN');

		scratch.innerHTML = '';

		render(<foo />, scratch);
		render(<x-bar />, scratch);
		expect(scratch.childNodes).to.have.length(2);
		expect(scratch.childNodes[0]).to.have.property('nodeName', 'FOO');
		expect(scratch.childNodes[1]).to.have.property('nodeName', 'X-BAR');
	});

	it('should nest empty nodes', () => {
		render((

				<foo />
				<x-bar />

		), scratch);

		expect(scratch.childNodes)
			.to.have.length(1)
			.and.to.have.deep.property('0.nodeName', 'DIV');

		let c = scratch.childNodes[0].childNodes;
		expect(c).to.have.length(3);
		expect(c).to.have.deep.property('0.nodeName', 'SPAN');
		expect(c).to.have.deep.property('1.nodeName', 'FOO');
		expect(c).to.have.deep.property('2.nodeName', 'X-BAR');
	});

	it('should apply string attributes', () => {
		render(
, scratch);

		let div = scratch.childNodes[0];
		expect(div).to.have.deep.property('attributes.length', 2);

		expect(div).to.have.deep.property('attributes[0].name', 'foo');
		expect(div).to.have.deep.property('attributes[0].value', 'bar');

		expect(div).to.have.deep.property('attributes[1].name', 'data-foo');
		expect(div).to.have.deep.property('attributes[1].value', 'databar');
	});

	it('should apply class as String', () => {
		render(
, scratch);
		expect(scratch.childNodes[0]).to.have.property('className', 'foo');
	});

	it('should alias className to class', () => {
		render(
, scratch);
		expect(scratch.childNodes[0]).to.have.property('className', 'bar');
	});

	it('should apply style as String', () => {
		render(
, scratch);
		expect(scratch.childNodes[0]).to.have.deep.property('style.cssText')
			.that.matches(/top\s*:\s*5px\s*/)
			.and.matches(/position\s*:\s*relative\s*/);
	});

	it('should only register on* functions as handlers', () => {
		let click = () => {},
			onclick = () => {};

		let proto = document.createElement('div').constructor.prototype;

		sinon.spy(proto, 'addEventListener');

		render(
, scratch);

		expect(scratch.childNodes[0]).to.have.deep.property('attributes.length', 0);

		expect(proto.addEventListener).to.have.been.calledOnce
			.and.to.have.been.calledWithExactly('click', sinon.match.func);

		proto.addEventListener.restore();
	});

	it('should add and remove event handlers', () => {
		let click = sinon.spy(),
			mousedown = sinon.spy();

		let proto = document.createElement('div').constructor.prototype;
		sinon.spy(proto, 'addEventListener');
		sinon.spy(proto, 'removeEventListener');

		function fireEvent(on, type) {
			let e = document.createEvent('Event');
			e.initEvent(type);
			on.dispatchEvent(e);
		}

		render(
 click(1) } onMouseDown={ mousedown } />, scratch);

		expect(proto.addEventListener).to.have.been.calledTwice
			.and.to.have.been.calledWith('click')
			.and.calledWith('mousedown');

		fireEvent(scratch.childNodes[0], 'click');
		expect(click).to.have.been.calledOnce
			.and.calledWith(1);

		proto.addEventListener.reset();
		click.reset();

		render(
 click(2) } />, scratch, scratch.firstChild);

		expect(proto.addEventListener).not.to.have.been.called;

		expect(proto.removeEventListener)
			.to.have.been.calledOnce
			.and.calledWith('mousedown');

		fireEvent(scratch.childNodes[0], 'click');
		expect(click).to.have.been.calledOnce
			.and.to.have.been.calledWith(2);

		fireEvent(scratch.childNodes[0], 'mousedown');
		expect(mousedown).not.to.have.been.called;

		proto.removeEventListener.reset();
		click.reset();
		mousedown.reset();

		render(
, scratch, scratch.firstChild);

		expect(proto.removeEventListener)
			.to.have.been.calledOnce
			.and.calledWith('click');

		fireEvent(scratch.childNodes[0], 'click');
		expect(click).not.to.have.been.called;

		proto.addEventListener.restore();
		proto.removeEventListener.restore();
	});

	it('should serialize style objects', () => {
		render(
, scratch);

		let { style } = scratch.childNodes[0];
		expect(style).to.have.property('color', 'rgb(255, 255, 255)');
		expect(style).to.have.property('background').that.contains('rgb(255, 100, 0)');
		expect(style).to.have.property('backgroundPosition').that.matches(/0(px)? 0(px)?/);
		expect(style).to.have.property('backgroundSize', 'cover');
		expect(style).to.have.property('padding', '5px');
		expect(style).to.have.property('top', '100px');
		expect(style).to.have.property('left', '100%');
	});

	it('should serialize class/className', () => {
		render(
, scratch);

		let { className } = scratch.childNodes[0];
		expect(className).to.be.a.string;
		expect(className.split(' '))
			.to.include.members(['yes1', 'yes2', 'yes3', 'yes4', 'yes5'])
			.and.not.include.members(['no1', 'no2', 'no3', 'no4', 'no5']);
	});

	it('should support dangerouslySetInnerHTML', () => {
		let html = 'foo & bar';
		render(
, scratch);

		expect(scratch.firstChild).to.have.property('innerHTML', html);
		expect(scratch.innerHTML).to.equal('
'+html+'
');
	});

	it('should reconcile mutated DOM attributes', () => {
		let check = p => render(<input type="checkbox" checked={p} />, scratch, scratch.lastChild),
			value = () => scratch.lastChild.checked,
			setValue = p => scratch.lastChild.checked = p;
		check(true);
		expect(value()).to.equal(true);
		check(false);
		expect(value()).to.equal(false);
		check(true);
		expect(value()).to.equal(true);
		setValue(true);
		check(false);
		expect(value()).to.equal(false);
		setValue(false);
		check(true);
		expect(value()).to.equal(true);
	});

	it('should render components', () => {
		class C1 extends Component {
			render() {
				return
C1
;
			}
		}
		sinon.spy(C1.prototype, 'render');
		render(<C1 />, scratch);

		expect(C1.prototype.render)
			.to.have.been.calledOnce
			.and.to.have.been.calledWithMatch({}, {})
			.and.to.have.returned(sinon.match({ nodeName:'div' }));

		expect(scratch.innerHTML).to.equal('
C1
');
	});

	it('should render components with props', () => {
		const PROPS = { foo:'bar', onBaz:()=>{} };
		let constructorProps;

		class C2 extends Component {
			constructor(props) {
				super(props);
				constructorProps = props;
			}
			render(props) {
				return
;
			}
		}
		sinon.spy(C2.prototype, 'render');

		render(<C2 {...PROPS} />, scratch);

		expect(constructorProps).to.deep.equal(PROPS);

		expect(C2.prototype.render)
			.to.have.been.calledOnce
			.and.to.have.been.calledWithMatch(PROPS, {})
			.and.to.have.returned(sinon.match({
				nodeName: 'div',
				attributes: PROPS
			}));

		expect(scratch.innerHTML).to.equal('

');
	});

	it('should render functional components', () => {
		const PROPS = { foo:'bar', onBaz:()=>{} };

		const C3 = sinon.spy( props =>
 );

		render(<C3 {...PROPS} />, scratch);

		expect(C3)
			.to.have.been.calledOnce
			.and.to.have.been.calledWith(PROPS)
			.and.to.have.returned(sinon.match({
				nodeName: 'div',
				attributes: PROPS
			}));

		expect(scratch.innerHTML).to.equal('

');
	});

	it('should render nested functional components', () => {
		const PROPS = { foo:'bar', onBaz:()=>{} };

		const Outer = sinon.spy(
			props => <Inner {...props} />
		);

		const Inner = sinon.spy(
			props =>
inner

		);

		render(<Outer {...PROPS} />, scratch);

		expect(Outer)
			.to.have.been.calledOnce
			.and.to.have.been.calledWith(PROPS)
			.and.to.have.returned(sinon.match({
				nodeName: Inner,
				attributes: PROPS
			}));

		expect(Inner)
			.to.have.been.calledOnce
			.and.to.have.been.calledWith(PROPS)
			.and.to.have.returned(sinon.match({
				nodeName: 'div',
				attributes: PROPS,
				children: ['inner']
			}));

		expect(scratch.innerHTML).to.equal('
inner
');
	});

	it('should re-render nested functional components', () => {
		let doRender = null;
		class Outer extends Component {
			componentDidMount() {
				let i = 1;
				doRender = () => this.setState({ i: ++i });
			}
			componentWillUnmount() {}
			render(props, { i }) {
				return <Inner i={i} {...props} />;
			}
		}
		sinon.spy(Outer.prototype, 'render');
		sinon.spy(Outer.prototype, 'componentWillUnmount');

		let j = 0;
		const Inner = sinon.spy(
			props =>
inner

		);

		render(<Outer foo="bar" />, scratch);

		// update & flush
		doRender();
		rerender();

		expect(Outer.prototype.componentWillUnmount)
			.not.to.have.been.called;

		expect(Inner).to.have.been.calledTwice;

		expect(Inner.secondCall)
			.to.have.been.calledWith({ foo:'bar', i:2 })
			.and.to.have.returned(sinon.match({
				attributes: {
					j: 2,
					i: 2,
					foo: 'bar'
				}
			}));

		expect(scratch.innerHTML).to.equal('
inner
');

		// update & flush
		doRender();
		rerender();

		expect(Inner).to.have.been.calledThrice;

		expect(Inner.thirdCall)
			.to.have.been.calledWith({ foo:'bar', i:3 })
			.and.to.have.returned(sinon.match({
				attributes: {
					j: 3,
					i: 3,
					foo: 'bar'
				}
			}));

		expect(scratch.innerHTML).to.equal('
inner
');
	});

	it('should re-render nested components', () => {
		let doRender = null,
			alt = false;

		class Outer extends Component {
			componentDidMount() {
				let i = 1;
				doRender = () => this.setState({ i: ++i });
			}
			componentWillUnmount() {}
			render(props, { i }) {
				if (alt) return
;
				return <Inner i={i} {...props} />;
			}
		}
		sinon.spy(Outer.prototype, 'render');
		sinon.spy(Outer.prototype, 'componentDidMount');
		sinon.spy(Outer.prototype, 'componentWillUnmount');

		let j = 0;
		class Inner extends Component {
			constructor(...args) {
				super();
				this._constructor(...args);
			}
			_constructor() {}
			componentWillMount() {}
			componentDidMount() {}
			componentWillUnmount() {}
			componentDidUnmount() {}
			render(props) {
				return
inner
;
			}
		}
		sinon.spy(Inner.prototype, '_constructor');
		sinon.spy(Inner.prototype, 'render');
		sinon.spy(Inner.prototype, 'componentWillMount');
		sinon.spy(Inner.prototype, 'componentDidMount');
		sinon.spy(Inner.prototype, 'componentDidUnmount');
		sinon.spy(Inner.prototype, 'componentWillUnmount');

		render(<Outer foo="bar" />, scratch);

		expect(Outer.prototype.componentDidMount).to.have.been.calledOnce;

		// update & flush
		doRender();
		rerender();

		expect(Outer.prototype.componentWillUnmount).not.to.have.been.called;

		expect(Inner.prototype._constructor).to.have.been.calledOnce;
		expect(Inner.prototype.componentWillUnmount).not.to.have.been.called;
		expect(Inner.prototype.componentDidUnmount).not.to.have.been.called;
		expect(Inner.prototype.componentWillMount).to.have.been.calledOnce;
		expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
		expect(Inner.prototype.render).to.have.been.calledTwice;

		expect(Inner.prototype.render.secondCall)
			.to.have.been.calledWith({ foo:'bar', i:2 })
			.and.to.have.returned(sinon.match({
				attributes: {
					j: 2,
					i: 2,
					foo: 'bar'
				}
			}));

		expect(scratch.innerHTML).to.equal('
inner
');

		// update & flush
		doRender();
		rerender();

		expect(Inner.prototype.componentWillUnmount).not.to.have.been.called;
		expect(Inner.prototype.componentDidUnmount).not.to.have.been.called;
		expect(Inner.prototype.componentWillMount).to.have.been.calledOnce;
		expect(Inner.prototype.componentDidMount).to.have.been.calledOnce;
		expect(Inner.prototype.render).to.have.been.calledThrice;

		expect(Inner.prototype.render.thirdCall)
			.to.have.been.calledWith({ foo:'bar', i:3 })
			.and.to.have.returned(sinon.match({
				attributes: {
					j: 3,
					i: 3,
					foo: 'bar'
				}
			}));

		expect(scratch.innerHTML).to.equal('
inner
');

		// update & flush
		alt = true;
		doRender();
		rerender();

		expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce;
		expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce;
	});

	// Test for Issue #73
	it('should remove orphaned elements replaced by Components', () => {
		class Comp extends Component {
			render() {
				return span in a component;
			}
		}

		let root;
		function test(content) {
			root = render(content, scratch, root);
		}

		test(<Comp />);
		test(
just a div
);
		test(<Comp />);

		expect(scratch.innerHTML).to.equal('span in a component');
	});
});


import options from './options';
import { setImmediate } from './util';
import { renderComponent } from './vdom/component';

/** Managed queue of dirty components to be re-rendered */

// items/itemsOffline swap on each rerender() call (just a simple pool technique)
let items = [],
	itemsOffline = [];

export function enqueueRender(component) {
	if (items.push(component)!==1) return;

	(options.debounceRendering || setImmediate)(rerender);
}

export function rerender() {
	if (!items.length) return;

	let currentItems = items,
		p;

	// swap online & offline
	items = itemsOffline;
	itemsOffline = currentItems;

	while ( (p = currentItems.pop()) ) {
		if (p._dirty) renderComponent(p);
	}
}


(function ($) {
  $.fn.collapsible = function(options) {
    var defaults = {
        accordion: undefined
    };

    options = $.extend(defaults, options);

    return this.each(function() {

      var $this = $(this);

      var $panel_headers = $(this).find('.collapsible-header');

      var collapsible_type = $this.data("collapsible");

      // Turn off any existing event handlers
       $this.off();
       $this.children().off();

       /****************
       Helper Functions
       ****************/

      // Accordion Open
      function accordionOpen(object) {
        $panel_headers = $this.find('.collapsible-header');
        object.parent().toggleClass('active');
        if (object.parent().hasClass('active')){
          object.siblings('.collapsible-body').stop(true,false).slideDown({ duration: 350, easing: "easeOutQuart", queue: false});
        }
        else{
          object.siblings('.collapsible-body').stop(true,false).slideUp({ duration: 350, easing: "easeOutQuart", queue: false});
        }
        $panel_headers.not(object).parent().removeClass('active');
        $panel_headers.not(object).parent().children('.collapsible-body').stop(true,false).slideUp({ duration: 350, easing: "easeOutQuart", queue: false});
      }
      // Collapsible Open
      function collapsibleOpen(object) {
        object.parent().toggleClass('active');
        if (object.parent().hasClass('active')){
          object.siblings('.collapsible-body').stop(true,false).slideDown({ duration: 350, easing: "easeOutQuart", queue: false});
        }
        else{
          object.siblings('.collapsible-body').stop(true,false).slideUp({ duration: 350, easing: "easeOutQuart", queue: false});
        }
      }

      /*****  End Helper Functions  *****/

      if (options.accordion || collapsible_type == "accordion" || collapsible_type == undefined) { // Handle Accordion

        // Event delegation to all collapsible section
        $this.on('click', '.collapsible-header', function (e) {
          accordionOpen($(e.currentTarget));
        });

        // Open first active
        accordionOpen($panel_headers.filter('.active').first());
      }
      else { // Handle Expandables
        $panel_headers.each(function () {

          // Event delegation to open collapsible section
          $(this).on('click', function (e) {
            collapsibleOpen($(e.currentTarget));
          });

          // Open any bodies that have the active class
          if ($(this).hasClass('active')) {
            collapsibleOpen($(this));
          }

        });
      }

    });
  };

  $(document).ready(function(){
    $('.collapsible').collapsible();
  });
}( jQuery ));


- var white_nav = true
- var page = "About"

doctype html
html
  head
    include _head.jade
  body
    include _navbar.jade
    main
      include about/about_content.html
    include _footer.html
    include _scripts.html