<commit_before>/*
 * Copyright (c) 2013 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, brackets, $ */

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

    var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
        EditorManager   = brackets.getModule("editor/EditorManager"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        Commands        = brackets.getModule("command/Commands"),
        CommandManager  = brackets.getModule("command/CommandManager"),
        Menus           = brackets.getModule("command/Menus"),
        Strings         = brackets.getModule("strings"),
        AppInit         = brackets.getModule("utils/AppInit"),
        ExtensionUtils  = brackets.getModule("utils/ExtensionUtils"),
        StringUtils     = brackets.getModule("utils/StringUtils"),
        HintUtils       = require("HintUtils"),
        ScopeManager    = require("ScopeManager"),
        Session         = require("Session");

    var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));

    var JUMPTO_DEFINITION = "navigate.jumptoDefinition";

    var session     = null,  // object that encapsulates the current session state
        cachedHints = null,  // sorted hints for the current hinting session
        cachedType  = null,  // describes the lookup type and the object context
        cachedLine  = null;  // the line number for the cached scope

    /**
     * Creates a hint response object. Filters the hint list using the query
     * string, formats the hints for display, and returns a hint response
     * object according to the CodeHintManager's API for code hint providers.
     *
     * @param {Array.<Object>} hints - hints to be included in the response
     * @param {string} query - querystring with which to filter the hint list
     * @param {Object} type - the type of query, property vs. identifier
     * @return {Object} - hint response as defined by the CodeHintManager API
     */
    function getHintResponse(hints, query, type) {

        var trimmedQuery,
            formattedHints;

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {Array.<jQuery.Object>} - array of hints formatted as jQuery
         *      objects
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var hint        = token.value,
                    index       = hint.toLowerCase().indexOf(query.toLowerCase()),
                    $hintObj    = $("<span>").addClass("brackets-js-hints");

                // level indicates either variable scope or property confidence
                if (!type.property && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

                if (token.guess) {
                    $hintObj.addClass("guess-hint");
                }

                // is the token a global variable?
                if (token.global) {
                    $hintObj.addClass("global-hint");
                }

                // is the token a keyword?
                if (token.keyword) {
                    $hintObj.addClass("keyword-hint");
                }

                // higlight the matched portion of each hint
                if (query.length > 0 && index >= 0) {
                    var prefix  = StringUtils.htmlEscape(hint.slice(0, index)),
                        match   = StringUtils.htmlEscape(hint.slice(index, index + query.length)),
                        suffix  = StringUtils.htmlEscape(hint.slice(index + query.length));

                    $hintObj.append(prefix)
                        .append($("<span>")
                                .append(match)
                                .addClass("matched-hint"))
                        .append(suffix);
                } else {
                    $hintObj.text(hint);
                }
                $hintObj.data("token", token);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        if (query.indexOf(HintUtils.SINGLE_QUOTE) === 0 ||
                query.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
            trimmedQuery = query.substring(1);
            if (trimmedQuery.lastIndexOf(HintUtils.DOUBLE_QUOTE) === trimmedQuery.length - 1 ||
                    trimmedQuery.lastIndexOf(HintUtils.SINGLE_QUOTE) === trimmedQuery.length - 1) {
                trimmedQuery = trimmedQuery.substring(0, trimmedQuery.length - 1);
            }
        } else {
            trimmedQuery = query;
        }

        if (hints) {
            formattedHints = formatHints(hints, trimmedQuery);
        } else {
            formattedHints = [];
        }

        return {
            hints: formattedHints,
            match: null, // the CodeHintManager should not format the results
            selectInitial: true
        };
    }

    /**
     * @constructor
     */
    function JSHints() {
    }

    /**
     * Determine whether hints are available for a given editor context
     *
     * @param {Editor} editor - the current editor context
     * @param {string} key - charCode of the last pressed key
     * @return {boolean} - can the provider provide hints for this session?
     */
    JSHints.prototype.hasHints = function (editor, key) {
        if (session && ((key === null) || HintUtils.maybeIdentifier(key))) {
            var cursor  = session.getCursor(),
                token   = session.getToken(cursor);

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                var offset = session.getOffset(),
                    type    = session.getType(),
                    query   = session.getQuery();

                // Invalidate cached information if: 1) no scope exists; 2) the
                // cursor has moved a line; 3) the scope is dirty; or 4) if the
                // cursor has moved into a different scope. Cached information
                // is also reset on editor change.
                if (!cachedHints ||
                        cachedLine !== cursor.line ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType) {
                    //console.log("clear hints");
                    cachedLine = null;
                    cachedHints = null;
                }
                return true;
            }
        }
        return false;
    };

    /**
      * Return a list of hints, possibly deferred, for the current editor
      * context
      *
      * @param {string} key - charCode of the last pressed key
      * @return {Object + jQuery.Deferred} - hint response (immediate or
      *     deferred) as defined by the CodeHintManager API
      */
    JSHints.prototype.getHints = function (key) {
        var cursor = session.getCursor(),
            token = session.getToken(cursor);
        if ((key === null) || HintUtils.hintable(token)) {
            if (token) {
                var type    = session.getType(),
                    query   = session.getQuery();

                // Compute fresh hints if none exist, or if the session
                // type has changed since the last hint computation
                if (!cachedHints ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType ||                                               query.length === 0) {
                    var offset          = session.getOffset(),
                        scopeResponse   = ScopeManager.requestHints(session, session.editor.document, offset),
                        self            = this;

                    if (scopeResponse.hasOwnProperty("promise")) {
                        var $deferredHints = $.Deferred();
                        scopeResponse.promise.done(function () {
                            cachedLine = cursor.line;
                            cachedType = session.getType();
                            cachedHints = session.getHints(query);

                            $(self).triggerHandler("resolvedResponse", [cachedHints, cachedType]);

                            if ($deferredHints.state() === "pending") {
                                query = session.getQuery();
                                var hintResponse    = getHintResponse(cachedHints, query, type);

                                $deferredHints.resolveWith(null, [hintResponse]);
                                $(self).triggerHandler("hintResponse", [query]);
                            }
                        }).fail(function () {
                            if ($deferredHints.state() === "pending") {
                                $deferredHints.reject();
                            }
                        });

                        $(this).triggerHandler("deferredResponse");
                        return $deferredHints;
                    } else {
                        cachedLine = cursor.line;
                    }
                }

                if (cachedHints) {
                    cachedHints = session.getHints(session.getQuery());
                    return getHintResponse(cachedHints, query, type);
                }
            }
        }

        return null;
    };

    /**
     * Inserts the hint selected by the user into the current editor.
     *
     * @param {jQuery.Object} hint - hint object to insert into current editor
     * @return {boolean} - should a new hinting session be requested
     *      immediately after insertion?
     */
    JSHints.prototype.insertHint = function ($hintObj) {
        var hint        = $hintObj.data("token"),
            completion  = hint.value,
            cursor      = session.getCursor(),
            token       = session.getToken(cursor),
            query       = session.getQuery(),
            start       = {line: cursor.line, ch: cursor.ch - query.length},
            end         = {line: cursor.line, ch: (token ? token.end : cursor.ch)},
            delimeter;

        if (token && token.string === ".") {
            var nextCursor  = session.getNextCursorOnLine(),
                nextToken   = nextCursor ? session.getToken(nextCursor) : null;

            if (nextToken && // don't replace delimiters, etc.
                    HintUtils.maybeIdentifier(nextToken.string) &&
                    HintUtils.hintable(nextToken)) {
                end.ch = nextToken.end;
            }
        }

        // If the hint is a string literal, choose a delimiter in which
        // to wrap it, preserving the existing delimiter if possible.
        if (hint.literal && hint.kind === "string") {
            if (token.string.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
                delimeter = HintUtils.DOUBLE_QUOTE;
            } else if (token.string.indexOf(HintUtils.SINGLE_QUOTE) === 0) {
                delimeter = HintUtils.SINGLE_QUOTE;
            } else {
                delimeter = hint.delimeter;
            }

            completion = completion.replace("\\", "\\\\");
            completion = completion.replace(delimeter, "\\" + delimeter);
            completion = delimeter + completion + delimeter;
        }

        if (session.getType().showFunctionType) {
            // function types show up as hints, so don't insert anything
            // if we were displaying a function type
            return false;
        }
        // Replace the current token with the completion
        session.editor.document.replaceRange(completion, start, end);

        // Return false to indicate that another hinting session is not needed
        return false;
    };

    // load the extension
    AppInit.appReady(function () {

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {Editor} editor - editor context to be initialized.
         */
        function initializeSession(editor) {
            ScopeManager.handleEditorChange(editor.document);
            session = new Session(editor);
        }

        /*
         * Install editor change listeners
         *
         * @param {Editor} editor - editor context on which to listen for
         *      changes
         */
        function installEditorListeners(editor) {
            // always clean up cached scope and hint info
            cachedLine = null;
            cachedHints = null;
            cachedType = null;

            if (editor && editor.getLanguageForSelection().getId() === HintUtils.LANGUAGE_ID) {
                initializeSession(editor);
                $(editor)
                    .on(HintUtils.eventName("change"), function () {
                        ScopeManager.handleFileChange(editor.document);
                    });
            } else {
                session = null;
            }
        }

        /*
         * Uninstall editor change listeners
         *
         * @param {Editor} editor - editor context on which to stop listening
         *      for changes
         */
        function uninstallEditorListeners(editor) {
            $(editor)
                .off(HintUtils.eventName("change"));
        }

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            uninstallEditorListeners(previous);
            installEditorListeners(current);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset     = session.getOffset(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            if (response.hasOwnProperty("promise")) {
                response.promise.done(function (jumpResp) {

                    if (jumpResp.resultFile !== jumpResp.file) {

                        var doc = DocumentManager.getCurrentDocument();
                        var x = doc.file.fullPath.lastIndexOf("/");
                        var dir;
                        if (x >= 0) {
                            dir = doc.file.fullPath.slice(0, x + 1);
                        } else {
                            dir = "";
                        }
                        CommandManager.execute(Commands.FILE_OPEN, { fullPath: dir+jumpResp.resultFile })
                            .done(function() {
                                session.editor.setCursorPos(jumpResp.start);
                                session.editor.setSelection(jumpResp.start, jumpResp.end);
                                session.editor.centerOnCursor();
                            });
                        return;
                    }

                    session.editor.setCursorPos(jumpResp.start);
                    session.editor.setSelection(jumpResp.start, jumpResp.end);
                    session.editor.centerOnCursor();

                }).fail(function () {
                    response.reject();
                });
            }
        }

        // Register command handler
        CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, JUMPTO_DEFINITION, handleJumpToDefinition);

        // Add the menu item
        var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU);
        if (menu) {
            menu.addMenuItem(JUMPTO_DEFINITION, KeyboardPrefs.jumptoDefinition, Menus.BEFORE, Commands.NAVIGATE_GOTO_DEFINITION);
        }

        ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");

        // uninstall/install change listener as the active editor changes
        $(EditorManager)
            .on(HintUtils.eventName("activeEditorChange"),
                handleActiveEditorChange);

        // immediately install the current editor
        installEditorListeners(EditorManager.getActiveEditor());

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID], 0);

        // for unit testing
        exports.jsHintProvider = jsHints;
        exports.initializeSession = initializeSession;
    });
});
<commit_msg>Fix Jump-To-Definition bug: trying to open new file when search fails.<commit_after>/*
 * Copyright (c) 2013 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, brackets, $ */

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

    var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
        EditorManager   = brackets.getModule("editor/EditorManager"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        Commands        = brackets.getModule("command/Commands"),
        CommandManager  = brackets.getModule("command/CommandManager"),
        Menus           = brackets.getModule("command/Menus"),
        Strings         = brackets.getModule("strings"),
        AppInit         = brackets.getModule("utils/AppInit"),
        ExtensionUtils  = brackets.getModule("utils/ExtensionUtils"),
        StringUtils     = brackets.getModule("utils/StringUtils"),
        HintUtils       = require("HintUtils"),
        ScopeManager    = require("ScopeManager"),
        Session         = require("Session");

    var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));

    var JUMPTO_DEFINITION = "navigate.jumptoDefinition";

    var session     = null,  // object that encapsulates the current session state
        cachedHints = null,  // sorted hints for the current hinting session
        cachedType  = null,  // describes the lookup type and the object context
        cachedLine  = null;  // the line number for the cached scope

    /**
     * Creates a hint response object. Filters the hint list using the query
     * string, formats the hints for display, and returns a hint response
     * object according to the CodeHintManager's API for code hint providers.
     *
     * @param {Array.<Object>} hints - hints to be included in the response
     * @param {string} query - querystring with which to filter the hint list
     * @param {Object} type - the type of query, property vs. identifier
     * @return {Object} - hint response as defined by the CodeHintManager API
     */
    function getHintResponse(hints, query, type) {

        var trimmedQuery,
            formattedHints;

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {Array.<jQuery.Object>} - array of hints formatted as jQuery
         *      objects
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var hint        = token.value,
                    index       = hint.toLowerCase().indexOf(query.toLowerCase()),
                    $hintObj    = $("<span>").addClass("brackets-js-hints");

                // level indicates either variable scope or property confidence
                if (!type.property && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

                if (token.guess) {
                    $hintObj.addClass("guess-hint");
                }

                // is the token a global variable?
                if (token.global) {
                    $hintObj.addClass("global-hint");
                }

                // is the token a keyword?
                if (token.keyword) {
                    $hintObj.addClass("keyword-hint");
                }

                // higlight the matched portion of each hint
                if (query.length > 0 && index >= 0) {
                    var prefix  = StringUtils.htmlEscape(hint.slice(0, index)),
                        match   = StringUtils.htmlEscape(hint.slice(index, index + query.length)),
                        suffix  = StringUtils.htmlEscape(hint.slice(index + query.length));

                    $hintObj.append(prefix)
                        .append($("<span>")
                                .append(match)
                                .addClass("matched-hint"))
                        .append(suffix);
                } else {
                    $hintObj.text(hint);
                }
                $hintObj.data("token", token);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        if (query.indexOf(HintUtils.SINGLE_QUOTE) === 0 ||
                query.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
            trimmedQuery = query.substring(1);
            if (trimmedQuery.lastIndexOf(HintUtils.DOUBLE_QUOTE) === trimmedQuery.length - 1 ||
                    trimmedQuery.lastIndexOf(HintUtils.SINGLE_QUOTE) === trimmedQuery.length - 1) {
                trimmedQuery = trimmedQuery.substring(0, trimmedQuery.length - 1);
            }
        } else {
            trimmedQuery = query;
        }

        if (hints) {
            formattedHints = formatHints(hints, trimmedQuery);
        } else {
            formattedHints = [];
        }

        return {
            hints: formattedHints,
            match: null, // the CodeHintManager should not format the results
            selectInitial: true
        };
    }

    /**
     * @constructor
     */
    function JSHints() {
    }

    /**
     * Determine whether hints are available for a given editor context
     *
     * @param {Editor} editor - the current editor context
     * @param {string} key - charCode of the last pressed key
     * @return {boolean} - can the provider provide hints for this session?
     */
    JSHints.prototype.hasHints = function (editor, key) {
        if (session && ((key === null) || HintUtils.maybeIdentifier(key))) {
            var cursor  = session.getCursor(),
                token   = session.getToken(cursor);

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                var offset = session.getOffset(),
                    type    = session.getType(),
                    query   = session.getQuery();

                // Invalidate cached information if: 1) no scope exists; 2) the
                // cursor has moved a line; 3) the scope is dirty; or 4) if the
                // cursor has moved into a different scope. Cached information
                // is also reset on editor change.
                if (!cachedHints ||
                        cachedLine !== cursor.line ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType) {
                    //console.log("clear hints");
                    cachedLine = null;
                    cachedHints = null;
                }
                return true;
            }
        }
        return false;
    };

    /**
      * Return a list of hints, possibly deferred, for the current editor
      * context
      *
      * @param {string} key - charCode of the last pressed key
      * @return {Object + jQuery.Deferred} - hint response (immediate or
      *     deferred) as defined by the CodeHintManager API
      */
    JSHints.prototype.getHints = function (key) {
        var cursor = session.getCursor(),
            token = session.getToken(cursor);
        if ((key === null) || HintUtils.hintable(token)) {
            if (token) {
                var type    = session.getType(),
                    query   = session.getQuery();

                // Compute fresh hints if none exist, or if the session
                // type has changed since the last hint computation
                if (!cachedHints ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType ||                                               query.length === 0) {
                    var offset          = session.getOffset(),
                        scopeResponse   = ScopeManager.requestHints(session, session.editor.document, offset),
                        self            = this;

                    if (scopeResponse.hasOwnProperty("promise")) {
                        var $deferredHints = $.Deferred();
                        scopeResponse.promise.done(function () {
                            cachedLine = cursor.line;
                            cachedType = session.getType();
                            cachedHints = session.getHints(query);

                            $(self).triggerHandler("resolvedResponse", [cachedHints, cachedType]);

                            if ($deferredHints.state() === "pending") {
                                query = session.getQuery();
                                var hintResponse    = getHintResponse(cachedHints, query, type);

                                $deferredHints.resolveWith(null, [hintResponse]);
                                $(self).triggerHandler("hintResponse", [query]);
                            }
                        }).fail(function () {
                            if ($deferredHints.state() === "pending") {
                                $deferredHints.reject();
                            }
                        });

                        $(this).triggerHandler("deferredResponse");
                        return $deferredHints;
                    } else {
                        cachedLine = cursor.line;
                    }
                }

                if (cachedHints) {
                    cachedHints = session.getHints(session.getQuery());
                    return getHintResponse(cachedHints, query, type);
                }
            }
        }

        return null;
    };

    /**
     * Inserts the hint selected by the user into the current editor.
     *
     * @param {jQuery.Object} hint - hint object to insert into current editor
     * @return {boolean} - should a new hinting session be requested
     *      immediately after insertion?
     */
    JSHints.prototype.insertHint = function ($hintObj) {
        var hint        = $hintObj.data("token"),
            completion  = hint.value,
            cursor      = session.getCursor(),
            token       = session.getToken(cursor),
            query       = session.getQuery(),
            start       = {line: cursor.line, ch: cursor.ch - query.length},
            end         = {line: cursor.line, ch: (token ? token.end : cursor.ch)},
            delimeter;

        if (token && token.string === ".") {
            var nextCursor  = session.getNextCursorOnLine(),
                nextToken   = nextCursor ? session.getToken(nextCursor) : null;

            if (nextToken && // don't replace delimiters, etc.
                    HintUtils.maybeIdentifier(nextToken.string) &&
                    HintUtils.hintable(nextToken)) {
                end.ch = nextToken.end;
            }
        }

        // If the hint is a string literal, choose a delimiter in which
        // to wrap it, preserving the existing delimiter if possible.
        if (hint.literal && hint.kind === "string") {
            if (token.string.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
                delimeter = HintUtils.DOUBLE_QUOTE;
            } else if (token.string.indexOf(HintUtils.SINGLE_QUOTE) === 0) {
                delimeter = HintUtils.SINGLE_QUOTE;
            } else {
                delimeter = hint.delimeter;
            }

            completion = completion.replace("\\", "\\\\");
            completion = completion.replace(delimeter, "\\" + delimeter);
            completion = delimeter + completion + delimeter;
        }

        if (session.getType().showFunctionType) {
            // function types show up as hints, so don't insert anything
            // if we were displaying a function type
            return false;
        }
        // Replace the current token with the completion
        session.editor.document.replaceRange(completion, start, end);

        // Return false to indicate that another hinting session is not needed
        return false;
    };

    // load the extension
    AppInit.appReady(function () {

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {Editor} editor - editor context to be initialized.
         */
        function initializeSession(editor) {
            ScopeManager.handleEditorChange(editor.document);
            session = new Session(editor);
        }

        /*
         * Install editor change listeners
         *
         * @param {Editor} editor - editor context on which to listen for
         *      changes
         */
        function installEditorListeners(editor) {
            // always clean up cached scope and hint info
            cachedLine = null;
            cachedHints = null;
            cachedType = null;

            if (editor && editor.getLanguageForSelection().getId() === HintUtils.LANGUAGE_ID) {
                initializeSession(editor);
                $(editor)
                    .on(HintUtils.eventName("change"), function () {
                        ScopeManager.handleFileChange(editor.document);
                    });
            } else {
                session = null;
            }
        }

        /*
         * Uninstall editor change listeners
         *
         * @param {Editor} editor - editor context on which to stop listening
         *      for changes
         */
        function uninstallEditorListeners(editor) {
            $(editor)
                .off(HintUtils.eventName("change"));
        }

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            uninstallEditorListeners(previous);
            installEditorListeners(current);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset     = session.getOffset(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            if (response.hasOwnProperty("promise")) {
                response.promise.done(function (jumpResp) {

                    if (jumpResp.resultFile && (jumpResp.resultFile !== jumpResp.file)) {

                        var doc = DocumentManager.getCurrentDocument();
                        var x = doc.file.fullPath.lastIndexOf("/");
                        var dir;
                        if (x >= 0) {
                            dir = doc.file.fullPath.slice(0, x + 1);
                        } else {
                            dir = "";
                        }
                        CommandManager.execute(Commands.FILE_OPEN, { fullPath: dir+jumpResp.resultFile })
                            .done(function() {
                                session.editor.setCursorPos(jumpResp.start);
                                session.editor.setSelection(jumpResp.start, jumpResp.end);
                                session.editor.centerOnCursor();
                            });
                        return;
                    }

                    session.editor.setCursorPos(jumpResp.start);
                    session.editor.setSelection(jumpResp.start, jumpResp.end);
                    session.editor.centerOnCursor();

                }).fail(function () {
                    response.reject();
                });
            }
        }

        // Register command handler
        CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, JUMPTO_DEFINITION, handleJumpToDefinition);

        // Add the menu item
        var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU);
        if (menu) {
            menu.addMenuItem(JUMPTO_DEFINITION, KeyboardPrefs.jumptoDefinition, Menus.BEFORE, Commands.NAVIGATE_GOTO_DEFINITION);
        }

        ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");

        // uninstall/install change listener as the active editor changes
        $(EditorManager)
            .on(HintUtils.eventName("activeEditorChange"),
                handleActiveEditorChange);

        // immediately install the current editor
        installEditorListeners(EditorManager.getActiveEditor());

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID], 0);

        // for unit testing
        exports.jsHintProvider = jsHints;
        exports.initializeSession = initializeSession;
    });
});
<|endoftext|>


<commit_before>/*
  Accessible through browser at location path static/foam2/src/foam/nanos/controller/index.html
  Available on browser console as ctrl. (exports axiom)
*/

foam.CLASS({
  package: 'foam.nanos.controller',
  name: 'ApplicationController',
  extends: 'foam.u2.Element',

  arequire: function() { return foam.nanos.client.ClientBuilder.create(); },

  documentation: 'FOAM Application Controller.',

  implements: [
    'foam.nanos.client.Client'
  ],

  requires: [
    'foam.nanos.auth.User',
    'foam.u2.stack.Stack',
    'foam.u2.stack.StackView'
  ],

  imports: [
    'installCSS',
    'sessionSuccess',
    'window'
  ],

  exports: [
    'as ctrl',
    'loginSuccess',
    'logo',
    'requestLogin',
    'signUpEnabled',
    'stack',
    'user',
    'webApp',
    'wrapCSS as installCSS'
  ],

  css: `
    body {
      font-family: 'Roboto', sans-serif;
      font-size: 14px;
      letter-spacing: 0.2px;
      color: #373a3c;
      background: #edf0f5;
      margin: 0;
    }

    .stack-wrapper {
      margin-bottom: -10px;
      min-height: calc(80% - 60px);
    }

    .stack-wrapper:after {
      content: "";
      display: block;
    }
  `,

  properties: [
    {
      name: 'stack',
      factory: function() { return this.Stack.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'user',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'Boolean',
      name: 'signUpEnabled',
      adapt: function(v) {
        return v === 'false' ? false : true;
      }
    },
    {
      class: 'Boolean',
      name: 'loginSuccess'
    },
    'logo',
    'webApp',
    'primaryColor',
    'secondaryColor',
    'tableColor',
    'accentColor'
  ],

  methods: [
    function init() {
      this.SUPER();
      var self = this;

      this.user.id$.sub(this.onUserUpdate);

      this.getCurrentUser();

      window.onpopstate = function(event) {
        if ( location.hash != null ) {
          var hid = location.hash.substr(1);

          hid && self.menuDAO.find(hid).then(function(menu) {
            menu && menu.launch(this, null);
          });
        }
      };

      window.onpopstate();
    },

    function initE() {
      this
        .addClass(this.myClass())
        .tag({class: 'foam.u2.navigation.TopNavigation'})
        .start('div').addClass('stack-wrapper')
          .tag({class: 'foam.u2.stack.StackView', data: this.stack, showActions: false})
        .end();
    },

    function setDefaultMenu() {
      var self = this;
      this.menuDAO.find(this.user.group).then(function (group) {
        self.window.location.hash = group.defaultMenu || 'dashboard';
      });
    },

    function getCurrentUser() {
      var self = this;

      // get current user, else show login
      this.auth.getCurrentUser(null).then(function (result) {
        self.loginSuccess = !! result;
        if ( result ) {
          self.user.copyFrom(result);
        }
      })
      .catch(function (err) {
        self.requestLogin().then(function() {
          self.getCurrentUser();
        });
      });
    },

    // CSS preprocessor, works on classes instantiated in subContext
    function wrapCSS(text, id) {
      if ( text ) {
        if ( ! this.accentColor ) {
          var self = this;

          this.accentColor$.sub(function(s) {
            self.wrapCSS(text, id);
            s.detach();
          });
        }

        this.installCSS(text.
          replace(/%PRIMARYCOLOR%/g,   this.primaryColor).
          replace(/%SECONDARYCOLOR%/g, this.secondaryColor).
          replace(/%TABLECOLOR%/g,     this.tableColor).
          replace(/%ACCENTCOLOR%/g,    this.accentColor),
          id);
      }
    },

    function requestLogin() {
      var self = this;

      // don't go to log in screen if going to reset password screen
      if ( location.hash != null && location.hash === '#reset' ) return;

      return new Promise(function(resolve, reject) {
        self.stack.push({ class: 'foam.nanos.auth.SignInView' });
        self.loginSuccess$.sub(resolve);
      });
    }
  ],

  listeners: [
    {
      name: 'onUserUpdate',
      isFramed: true,
      code: function() {
        this.setDefaultMenu();
      }
    }
  ]
});
<commit_msg>Fixed bug in setting defaultMenu.<commit_after>/*
  Accessible through browser at location path static/foam2/src/foam/nanos/controller/index.html
  Available on browser console as ctrl. (exports axiom)
*/

foam.CLASS({
  package: 'foam.nanos.controller',
  name: 'ApplicationController',
  extends: 'foam.u2.Element',

  arequire: function() { return foam.nanos.client.ClientBuilder.create(); },

  documentation: 'FOAM Application Controller.',

  implements: [
    'foam.nanos.client.Client'
  ],

  requires: [
    'foam.nanos.auth.User',
    'foam.u2.stack.Stack',
    'foam.u2.stack.StackView'
  ],

  imports: [
    'installCSS',
    'sessionSuccess',
    'window'
  ],

  exports: [
    'as ctrl',
    'loginSuccess',
    'logo',
    'requestLogin',
    'signUpEnabled',
    'stack',
    'user',
    'webApp',
    'wrapCSS as installCSS'
  ],

  css: `
    body {
      font-family: 'Roboto', sans-serif;
      font-size: 14px;
      letter-spacing: 0.2px;
      color: #373a3c;
      background: #edf0f5;
      margin: 0;
    }

    .stack-wrapper {
      margin-bottom: -10px;
      min-height: calc(80% - 60px);
    }

    .stack-wrapper:after {
      content: "";
      display: block;
    }
  `,

  properties: [
    {
      name: 'stack',
      factory: function() { return this.Stack.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'user',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'Boolean',
      name: 'signUpEnabled',
      adapt: function(v) {
        return v === 'false' ? false : true;
      }
    },
    {
      class: 'Boolean',
      name: 'loginSuccess'
    },
    'logo',
    'webApp',
    'primaryColor',
    'secondaryColor',
    'tableColor',
    'accentColor'
  ],

  methods: [
    function init() {
      this.SUPER();
      var self = this;

      this.user.id$.sub(this.onUserUpdate);

      this.getCurrentUser();

      window.onpopstate = function(event) {
        if ( location.hash != null ) {
          var hid = location.hash.substr(1);

          hid && self.menuDAO.find(hid).then(function(menu) {
            menu && menu.launch(this, null);
          });
        }
      };

      window.onpopstate();
    },

    function initE() {
      this
        .addClass(this.myClass())
        .tag({class: 'foam.u2.navigation.TopNavigation'})
        .start('div').addClass('stack-wrapper')
          .tag({class: 'foam.u2.stack.StackView', data: this.stack, showActions: false})
        .end();
    },

    function setDefaultMenu() {
      var self = this;
      this.groupDAO.find(this.user.group).then(function (group) {
        self.window.location.hash = group.defaultMenu;
      });
    },

    function getCurrentUser() {
      var self = this;

      // get current user, else show login
      this.auth.getCurrentUser(null).then(function (result) {
        self.loginSuccess = !! result;
        if ( result ) {
          self.user.copyFrom(result);
        }
      })
      .catch(function (err) {
        self.requestLogin().then(function() {
          self.getCurrentUser();
        });
      });
    },

    // CSS preprocessor, works on classes instantiated in subContext
    function wrapCSS(text, id) {
      if ( text ) {
        if ( ! this.accentColor ) {
          var self = this;

          this.accentColor$.sub(function(s) {
            self.wrapCSS(text, id);
            s.detach();
          });
        }

        this.installCSS(text.
          replace(/%PRIMARYCOLOR%/g,   this.primaryColor).
          replace(/%SECONDARYCOLOR%/g, this.secondaryColor).
          replace(/%TABLECOLOR%/g,     this.tableColor).
          replace(/%ACCENTCOLOR%/g,    this.accentColor),
          id);
      }
    },

    function requestLogin() {
      var self = this;

      // don't go to log in screen if going to reset password screen
      if ( location.hash != null && location.hash === '#reset' ) return;

      return new Promise(function(resolve, reject) {
        self.stack.push({ class: 'foam.nanos.auth.SignInView' });
        self.loginSuccess$.sub(resolve);
      });
    }
  ],

  listeners: [
    {
      name: 'onUserUpdate',
      isFramed: true,
      code: function() {
        this.setDefaultMenu();
      }
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
  Accessible through browser at location path static/foam2/src/foam/nanos/controller/index.html
  Available on browser console as ctrl. (exports axiom)
*/
foam.CLASS({
  package: 'foam.nanos.controller',
  name: 'ApplicationController',
  extends: 'foam.u2.Element',

  documentation: 'FOAM Application Controller.',

  implements: [
    'foam.box.Context',
    'foam.mlang.Expressions',
    'foam.nanos.controller.AppStyles'
  ],

  requires: [
    'foam.nanos.client.ClientBuilder',
    'foam.nanos.auth.Group',
    'foam.nanos.auth.ResendVerificationEmail',
    'foam.nanos.auth.User',
    'foam.nanos.theme.Theme',
    'foam.nanos.theme.ThemeDomain',
    'foam.nanos.u2.navigation.TopNavigation',
    'foam.nanos.u2.navigation.FooterView',
    'foam.u2.stack.Stack',
    'foam.u2.stack.StackView',
    'foam.u2.dialog.NotificationMessage',
    'foam.nanos.session.SessionTimer',
    'foam.u2.dialog.Popup'
  ],

  imports: [
    'installCSS',
    'sessionSuccess',
    'window',
  ],

  exports: [
    'displayWidth',
    'agent',
    'appConfig',
    'as ctrl',
    'currentMenu',
    'group',
    'lastMenuLaunched',
    'lastMenuLaunchedListener',
    'loginSuccess',
    'theme',
    'menuListener',
    'notify',
    'pushMenu',
    'requestLogin',
    'signUpEnabled',
    'loginVariables',
    'stack',
    'user',
    'webApp',
    'wrapCSS as installCSS',
    'sessionTimer'
  ],

  constants: {
    MACROS: [
      'logoBackgroundColour',
      'customCSS',
      'primary1',
      'primary2',
      'primary3',
      'primary4',
      'primary5',
      'approval1',
      'approval2',
      'approval3',
      'approval4',
      'approval5',
      'warning1',
      'warning2',
      'warning3',
      'warning4',
      'warning5',
      'destructive1',
      'destructive2',
      'destructive3',
      'destructive4',
      'destructive5',
      'grey1',
      'grey2',
      'grey3',
      'grey4',
      'grey5',
      'black',
      'inputHeight',
      'inputVerticalPadding',
      'inputHorizontalPadding'
    ]
  },

  messages: [
    { name: 'GROUP_FETCH_ERR', message: 'Error fetching group' },
    { name: 'GROUP_NULL_ERR', message: 'Group was null' },
    { name: 'LOOK_AND_FEEL_NOT_FOUND', message: 'Could not fetch look and feel object.' }
  ],

  css: `
    body {
      font-family: 'Roboto', sans-serif;
      font-size: 14px;
      letter-spacing: 0.2px;
      color: #373a3c;
      background: /*%GREY5%*/ #f5f7fa;
      margin: 0;
    }
    .stack-wrapper {
      margin-bottom: -10px;
      min-height: calc(80% - 60px);
    }
    .stack-wrapper:after {
      content: "";
      display: block;
    }

    .truncate-ellipsis {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  `,

  properties: [
    {
      name: 'loginVariables',
      expression: function(client$userDAO) {
        return {
          dao_: client$userDAO || null,
          imgPath: '',
          group: 'system',
          countryChoices_: [] // empty defaults to entire countryDAO
        };
      }
    },
    {
      class: 'Enum',
      of: 'foam.u2.layout.DisplayWidth',
      name: 'displayWidth',
      value: foam.u2.layout.DisplayWidth.XL
    },
    {
      name: 'clientPromise',
      factory: function() {
        var self = this;
        return self.ClientBuilder.create().promise.then(function(cls) {
          self.client = cls.create(null, self);
          return self.client;
        });
      },
    },
    {
      name: 'client',
    },
    {
      name: 'appConfig',
      expression: function(client$appConfig) {
        return client$appConfig || null;
      }
    },
    {
      name: 'stack',
      factory: function() { return this.Stack.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'user',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'agent',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.Group',
      name: 'group'
    },
    {
      class: 'Boolean',
      name: 'signUpEnabled',
      adapt: function(_, v) {
        return foam.String.isInstance(v) ? v !== 'false' : v;
      }
    },
    {
      class: 'Boolean',
      name: 'loginSuccess'
    },
    {
      class: 'FObjectProperty',
      of: 'foam.nanos.session.SessionTimer',
      name: 'sessionTimer',
      factory: function() {
        return this.SessionTimer.create();
      }
    },
    {
      class: 'FObjectProperty',
      of: 'foam.nanos.theme.Theme',
      name: 'theme'
    },
    {
      class: 'foam.u2.ViewSpec',
      name: 'topNavigation_',
      factory: function() {
        return this.TopNavigation;
      }
    },
    {
      class: 'foam.u2.ViewSpec',
      name: 'footerView_',
      factory: function() {
        return this.FooterView;
      }
    },
    'currentMenu',
    'lastMenuLaunched',
    'webApp'
  ],

  methods: [
    function init() {
      this.SUPER();

      // done to start using SectionedDetailViews instead of DetailViews
      this.__subContext__.register(foam.u2.detail.SectionedDetailView, 'foam.u2.DetailView');

      var self = this;

      window.onpopstate = async function(event) {
        var hid = location.hash.substr(1);
        if ( hid ) {
          if ( self.client ) {
            var menu = await self.client.menuDAO.find(hid);
            menu && menu.launch(this);
          } else {
            self.clientPromise.then(async () => {
              var menu = await self.client.menuDAO.find(hid);
              menu && menu.launch(this);
            });
          }
        }
      };

      this.clientPromise.then(async function(client) {
        self.setPrivate_('__subContext__', client.__subContext__);

        await self.fetchAgent();
        await self.fetchUser();

        // Fetch the group only once the user has logged in. That's why we await
        // the line above before executing this one.
        await self.fetchGroup();
        self.onUserAgentAndGroupLoaded();
      });
    },

    function initE() {
      window.addEventListener('resize', this.updateDisplayWidth);
      this.updateDisplayWidth();

      this.clientPromise.then(() => {
        this.fetchTheme().then(() => {
          this
            .addClass(this.myClass())
            .start()
              .tag(this.topNavigation_)
            .end()
            .start()
              .addClass('stack-wrapper')
              .tag({
                class: 'foam.u2.stack.StackView',
                data: this.stack,
                showActions: false
              })
            .end()
            .start()
              .tag(this.footerView_)
            .end();
          });
      });
    },

    async function fetchGroup() {
      try {
        var group = await this.client.auth.getCurrentGroup();
        if ( group == null ) throw new Error(this.GROUP_NULL_ERR);
        this.group = group;
      } catch (err) {
        this.notify(this.GROUP_FETCH_ERR, 'error');
        console.error(err.message || this.GROUP_FETCH_ERR);
      }
    },

    async function fetchUser() {
      /** Get current user, else show login. */
      try {
        var result = await this.client.auth.getCurrentUser(null);
        this.loginSuccess = !! result;

        if ( ! result ) throw new Error();

        this.user = result;
      } catch (err) {
        await this.requestLogin();
        return await this.fetchUser();
      }
    },

    async function fetchAgent() {
      this.agent = await this.client.agentAuth.getCurrentAgent();
    },

    function expandShortFormMacro(css, m) {
      /* A short-form macros is of the form %PRIMARY_COLOR%. */
      var M = m.toUpperCase();

      // NOTE: We add a negative lookahead for */, which is used to close a
      // comment in CSS. We do this because if we don't, then when a developer
      // chooses to include a long form CSS macro directly in their CSS such as
      //
      //                       /*%EXAMPLE%*/ #abc123
      //
      // then we don't want this method to expand the commented portion of that
      // CSS because it's already in long form. By checking if */ follows the
      // macro, we can tell if it's already in long form and skip it.
      return css.replace(
        new RegExp('%' + M + '%(?!\\*/)', 'g'),
        '/*%' + M + '%*/ ' + this.theme[m]);
    },

    function expandLongFormMacro(css, m) {
      // A long-form macros is of the form "/*%PRIMARY_COLOR%*/ blue".
      var M = m.toUpperCase();

      return css.replace(
        new RegExp('/\\*%' + M + '%\\*/[^;]*', 'g'),
        '/*%' + M + '%*/ ' + this.theme[m]);
    },

    function wrapCSS(text, id) {
      /** CSS preprocessor, works on classes instantiated in subContext. */
      if ( text ) {
        var eid = foam.u2.Element.NEXT_ID();

        for ( var i = 0 ; i < this.MACROS.length ; i++ ) {
          let m     = this.MACROS[i];
          var text2 = this.expandShortFormMacro(this.expandLongFormMacro(text, m), m);

            // If the macro was found, then listen for changes to the property
            // and update the CSS if it changes.
            if ( text != text2 ) {
              text = text2;
              this.onDetach(this.theme$.dot(m).sub(() => {
                var el = this.getElementById(eid);
                el.innerText = this.expandLongFormMacro(el.innerText, m);
              }));
            }
        }

        this.installCSS(text, id, eid);
      }
    },

    function pushMenu(menuId) {
      /** Use to load a specific menu. **/
      if ( window.location.hash.substr(1) != menuId ) {
        window.location.hash = menuId;
      }
    },

    function requestLogin() {
      var self = this;

      // don't go to log in screen if going to reset password screen
      if ( location.hash && location.hash === '#reset' ) {
        return new Promise(function(resolve, reject) {
          self.stack.push({
            class: 'foam.nanos.auth.ChangePasswordView',
            modelOf: 'foam.nanos.auth.ResetPassword'
           });
          self.loginSuccess$.sub(resolve);
        });
      }

      return new Promise(function(resolve, reject) {
        self.stack.push({ class: 'foam.u2.view.LoginView', mode_: 'SignIn' }, self);
        self.loginSuccess$.sub(resolve);
      });
    },

    function notify(data, type) {
      /** Convenience method to create toast notifications. */
      this.add(this.NotificationMessage.create({
        message: data,
        type: type
      }));
    }
  ],

  listeners: [
    function onUserAgentAndGroupLoaded() {
      /**
       * Called whenever the group updates.
       *   - Updates the portal view based on the group
       *   - Update the look and feel of the app based on the group or user
       *   - Go to a menu based on either the hash or the group
       */
      if ( ! this.user.emailVerified ) {
        this.loginSuccess = false;
        this.stack.push({ class: 'foam.nanos.auth.ResendVerificationEmail' });
        return;
      }

      var hash = this.window.location.hash;
      if ( hash ) hash = hash.substring(1);

      if ( hash ) {
        window.onpopstate();
      } else if ( this.group ) {
        this.window.location.hash = this.group.defaultMenu;
      }

      // Update the look and feel now that the user is logged in since there
      // might be a more specific one to use now.
      this.fetchTheme();
    },

    function menuListener(m) {
      /**
       * This listener should be called when a Menu item has been launched
       * by some Menu View. Is exported.
       */
      this.currentMenu = m;
    },

    function lastMenuLaunchedListener(m) {
      /**
       * This listener should be called when a Menu has been launched but does
       * not navigate to a new screen. Typically for SubMenus.
       */
      this.lastMenuLaunched = m;
    },

    async function fetchTheme() {
      /**
       * Get the most appropriate Theme object from the server and use it to
       * customize the look and feel of the application.
       */
      var lastTheme = this.theme;

      try {
        if ( this.user && this.user.personalTheme ) {
          // If the user has a personal theme, use that.
          this.theme = await this.user.personalTheme$find;
        } else {
          var domain = window.location.hostname;

          var themeDomain = await this.client.themeDomainDAO.find(domain);
          if ( ! themeDomain ) {
            console.warn('Domain not found: '+domain);
            themeDomain = this.ThemeDomain.create({'theme':'foam'});
          }

          var predicate = this.AND(
            this.EQ(this.Theme.ID, themeDomain.theme),
            this.EQ(this.Theme.ENABLED, true)
          );

          this.theme = await this.client.themeDAO.find(predicate);
          if ( ! this.theme ) {
            console.warn('Theme not found: '+domain);
            return this.Theme.create({});
          }
        }
      } catch (err) {
        this.notify(this.LOOK_AND_FEEL_NOT_FOUND, 'error');
        console.error(err);
        return;
      }

      if ( ! lastTheme || lastTheme.id != this.theme.id ) this.useCustomElements();
    },

    function useCustomElements() {
      /** Use custom elements if supplied by the Theme. */
      if ( ! this.theme ) throw new Error(this.LOOK_AND_FEEL_NOT_FOUND);

      if ( this.theme.topNavigation ) {
        this.topNavigation_ = this.theme.topNavigation;
      }

      if ( this.theme.footerView ) {
        this.footerView_ = this.theme.footerView;
      }
    },
    {
      name: 'updateDisplayWidth',
      isMerged: true,
      mergeDelay: 1000,
      code: function() {
        this.displayWidth = foam.u2.layout.DisplayWidth.VALUES
          .concat()
          .sort((a, b) => b.minWidth - a.minWidth)
          .find(o => o.minWidth <= window.innerWidth);
      }
    }
  ]
});
<commit_msg>Fall through to domain theme if user personal theme not found.<commit_after>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
  Accessible through browser at location path static/foam2/src/foam/nanos/controller/index.html
  Available on browser console as ctrl. (exports axiom)
*/
foam.CLASS({
  package: 'foam.nanos.controller',
  name: 'ApplicationController',
  extends: 'foam.u2.Element',

  documentation: 'FOAM Application Controller.',

  implements: [
    'foam.box.Context',
    'foam.mlang.Expressions',
    'foam.nanos.controller.AppStyles'
  ],

  requires: [
    'foam.nanos.client.ClientBuilder',
    'foam.nanos.auth.Group',
    'foam.nanos.auth.ResendVerificationEmail',
    'foam.nanos.auth.User',
    'foam.nanos.theme.Theme',
    'foam.nanos.theme.ThemeDomain',
    'foam.nanos.u2.navigation.TopNavigation',
    'foam.nanos.u2.navigation.FooterView',
    'foam.u2.stack.Stack',
    'foam.u2.stack.StackView',
    'foam.u2.dialog.NotificationMessage',
    'foam.nanos.session.SessionTimer',
    'foam.u2.dialog.Popup'
  ],

  imports: [
    'installCSS',
    'sessionSuccess',
    'window',
  ],

  exports: [
    'displayWidth',
    'agent',
    'appConfig',
    'as ctrl',
    'currentMenu',
    'group',
    'lastMenuLaunched',
    'lastMenuLaunchedListener',
    'loginSuccess',
    'theme',
    'menuListener',
    'notify',
    'pushMenu',
    'requestLogin',
    'signUpEnabled',
    'loginVariables',
    'stack',
    'user',
    'webApp',
    'wrapCSS as installCSS',
    'sessionTimer'
  ],

  constants: {
    MACROS: [
      'logoBackgroundColour',
      'customCSS',
      'primary1',
      'primary2',
      'primary3',
      'primary4',
      'primary5',
      'approval1',
      'approval2',
      'approval3',
      'approval4',
      'approval5',
      'warning1',
      'warning2',
      'warning3',
      'warning4',
      'warning5',
      'destructive1',
      'destructive2',
      'destructive3',
      'destructive4',
      'destructive5',
      'grey1',
      'grey2',
      'grey3',
      'grey4',
      'grey5',
      'black',
      'inputHeight',
      'inputVerticalPadding',
      'inputHorizontalPadding'
    ]
  },

  messages: [
    { name: 'GROUP_FETCH_ERR', message: 'Error fetching group' },
    { name: 'GROUP_NULL_ERR', message: 'Group was null' },
    { name: 'LOOK_AND_FEEL_NOT_FOUND', message: 'Could not fetch look and feel object.' }
  ],

  css: `
    body {
      font-family: 'Roboto', sans-serif;
      font-size: 14px;
      letter-spacing: 0.2px;
      color: #373a3c;
      background: /*%GREY5%*/ #f5f7fa;
      margin: 0;
    }
    .stack-wrapper {
      margin-bottom: -10px;
      min-height: calc(80% - 60px);
    }
    .stack-wrapper:after {
      content: "";
      display: block;
    }

    .truncate-ellipsis {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  `,

  properties: [
    {
      name: 'loginVariables',
      expression: function(client$userDAO) {
        return {
          dao_: client$userDAO || null,
          imgPath: '',
          group: 'system',
          countryChoices_: [] // empty defaults to entire countryDAO
        };
      }
    },
    {
      class: 'Enum',
      of: 'foam.u2.layout.DisplayWidth',
      name: 'displayWidth',
      value: foam.u2.layout.DisplayWidth.XL
    },
    {
      name: 'clientPromise',
      factory: function() {
        var self = this;
        return self.ClientBuilder.create().promise.then(function(cls) {
          self.client = cls.create(null, self);
          return self.client;
        });
      },
    },
    {
      name: 'client',
    },
    {
      name: 'appConfig',
      expression: function(client$appConfig) {
        return client$appConfig || null;
      }
    },
    {
      name: 'stack',
      factory: function() { return this.Stack.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'user',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.User',
      name: 'agent',
      factory: function() { return this.User.create(); }
    },
    {
      class: 'foam.core.FObjectProperty',
      of: 'foam.nanos.auth.Group',
      name: 'group'
    },
    {
      class: 'Boolean',
      name: 'signUpEnabled',
      adapt: function(_, v) {
        return foam.String.isInstance(v) ? v !== 'false' : v;
      }
    },
    {
      class: 'Boolean',
      name: 'loginSuccess'
    },
    {
      class: 'FObjectProperty',
      of: 'foam.nanos.session.SessionTimer',
      name: 'sessionTimer',
      factory: function() {
        return this.SessionTimer.create();
      }
    },
    {
      class: 'FObjectProperty',
      of: 'foam.nanos.theme.Theme',
      name: 'theme'
    },
    {
      class: 'foam.u2.ViewSpec',
      name: 'topNavigation_',
      factory: function() {
        return this.TopNavigation;
      }
    },
    {
      class: 'foam.u2.ViewSpec',
      name: 'footerView_',
      factory: function() {
        return this.FooterView;
      }
    },
    'currentMenu',
    'lastMenuLaunched',
    'webApp'
  ],

  methods: [
    function init() {
      this.SUPER();

      // done to start using SectionedDetailViews instead of DetailViews
      this.__subContext__.register(foam.u2.detail.SectionedDetailView, 'foam.u2.DetailView');

      var self = this;

      window.onpopstate = async function(event) {
        var hid = location.hash.substr(1);
        if ( hid ) {
          if ( self.client ) {
            var menu = await self.client.menuDAO.find(hid);
            menu && menu.launch(this);
          } else {
            self.clientPromise.then(async () => {
              var menu = await self.client.menuDAO.find(hid);
              menu && menu.launch(this);
            });
          }
        }
      };

      this.clientPromise.then(async function(client) {
        self.setPrivate_('__subContext__', client.__subContext__);

        await self.fetchAgent();
        await self.fetchUser();

        // Fetch the group only once the user has logged in. That's why we await
        // the line above before executing this one.
        await self.fetchGroup();
        self.onUserAgentAndGroupLoaded();
      });
    },

    function initE() {
      window.addEventListener('resize', this.updateDisplayWidth);
      this.updateDisplayWidth();

      this.clientPromise.then(() => {
        this.fetchTheme().then(() => {
          this
            .addClass(this.myClass())
            .start()
              .tag(this.topNavigation_)
            .end()
            .start()
              .addClass('stack-wrapper')
              .tag({
                class: 'foam.u2.stack.StackView',
                data: this.stack,
                showActions: false
              })
            .end()
            .start()
              .tag(this.footerView_)
            .end();
          });
      });
    },

    async function fetchGroup() {
      try {
        var group = await this.client.auth.getCurrentGroup();
        if ( group == null ) throw new Error(this.GROUP_NULL_ERR);
        this.group = group;
      } catch (err) {
        this.notify(this.GROUP_FETCH_ERR, 'error');
        console.error(err.message || this.GROUP_FETCH_ERR);
      }
    },

    async function fetchUser() {
      /** Get current user, else show login. */
      try {
        var result = await this.client.auth.getCurrentUser(null);
        this.loginSuccess = !! result;

        if ( ! result ) throw new Error();

        this.user = result;
      } catch (err) {
        await this.requestLogin();
        return await this.fetchUser();
      }
    },

    async function fetchAgent() {
      this.agent = await this.client.agentAuth.getCurrentAgent();
    },

    function expandShortFormMacro(css, m) {
      /* A short-form macros is of the form %PRIMARY_COLOR%. */
      var M = m.toUpperCase();

      // NOTE: We add a negative lookahead for */, which is used to close a
      // comment in CSS. We do this because if we don't, then when a developer
      // chooses to include a long form CSS macro directly in their CSS such as
      //
      //                       /*%EXAMPLE%*/ #abc123
      //
      // then we don't want this method to expand the commented portion of that
      // CSS because it's already in long form. By checking if */ follows the
      // macro, we can tell if it's already in long form and skip it.
      return css.replace(
        new RegExp('%' + M + '%(?!\\*/)', 'g'),
        '/*%' + M + '%*/ ' + this.theme[m]);
    },

    function expandLongFormMacro(css, m) {
      // A long-form macros is of the form "/*%PRIMARY_COLOR%*/ blue".
      var M = m.toUpperCase();

      return css.replace(
        new RegExp('/\\*%' + M + '%\\*/[^;]*', 'g'),
        '/*%' + M + '%*/ ' + this.theme[m]);
    },

    function wrapCSS(text, id) {
      /** CSS preprocessor, works on classes instantiated in subContext. */
      if ( text ) {
        var eid = foam.u2.Element.NEXT_ID();

        for ( var i = 0 ; i < this.MACROS.length ; i++ ) {
          let m     = this.MACROS[i];
          var text2 = this.expandShortFormMacro(this.expandLongFormMacro(text, m), m);

            // If the macro was found, then listen for changes to the property
            // and update the CSS if it changes.
            if ( text != text2 ) {
              text = text2;
              this.onDetach(this.theme$.dot(m).sub(() => {
                var el = this.getElementById(eid);
                el.innerText = this.expandLongFormMacro(el.innerText, m);
              }));
            }
        }

        this.installCSS(text, id, eid);
      }
    },

    function pushMenu(menuId) {
      /** Use to load a specific menu. **/
      if ( window.location.hash.substr(1) != menuId ) {
        window.location.hash = menuId;
      }
    },

    function requestLogin() {
      var self = this;

      // don't go to log in screen if going to reset password screen
      if ( location.hash && location.hash === '#reset' ) {
        return new Promise(function(resolve, reject) {
          self.stack.push({
            class: 'foam.nanos.auth.ChangePasswordView',
            modelOf: 'foam.nanos.auth.ResetPassword'
           });
          self.loginSuccess$.sub(resolve);
        });
      }

      return new Promise(function(resolve, reject) {
        self.stack.push({ class: 'foam.u2.view.LoginView', mode_: 'SignIn' }, self);
        self.loginSuccess$.sub(resolve);
      });
    },

    function notify(data, type) {
      /** Convenience method to create toast notifications. */
      this.add(this.NotificationMessage.create({
        message: data,
        type: type
      }));
    }
  ],

  listeners: [
    function onUserAgentAndGroupLoaded() {
      /**
       * Called whenever the group updates.
       *   - Updates the portal view based on the group
       *   - Update the look and feel of the app based on the group or user
       *   - Go to a menu based on either the hash or the group
       */
      if ( ! this.user.emailVerified ) {
        this.loginSuccess = false;
        this.stack.push({ class: 'foam.nanos.auth.ResendVerificationEmail' });
        return;
      }

      var hash = this.window.location.hash;
      if ( hash ) hash = hash.substring(1);

      if ( hash ) {
        window.onpopstate();
      } else if ( this.group ) {
        this.window.location.hash = this.group.defaultMenu;
      }

      // Update the look and feel now that the user is logged in since there
      // might be a more specific one to use now.
      this.fetchTheme();
    },

    function menuListener(m) {
      /**
       * This listener should be called when a Menu item has been launched
       * by some Menu View. Is exported.
       */
      this.currentMenu = m;
    },

    function lastMenuLaunchedListener(m) {
      /**
       * This listener should be called when a Menu has been launched but does
       * not navigate to a new screen. Typically for SubMenus.
       */
      this.lastMenuLaunched = m;
    },

    async function fetchTheme() {
      /**
       * Get the most appropriate Theme object from the server and use it to
       * customize the look and feel of the application.
       */
      var lastTheme = this.theme;

      try {
        if ( this.user && this.user.personalTheme ) {
          // If the user has a personal theme, use that.
          this.theme = await this.user.personalTheme$find;
          if ( ! this.theme ) {
            console.warn('User Theme not found: '+this.user.id);
          }
        }
        if ( ! this.theme ) {
          var domain = window.location.hostname;

          var themeDomain = await this.client.themeDomainDAO.find(domain);
          if ( ! themeDomain ) {
            console.warn('ThemeDomain not found: '+domain);
            themeDomain = this.ThemeDomain.create({'theme':'foam'});
          }

          var predicate = this.AND(
            this.EQ(this.Theme.ID, themeDomain.theme),
            this.EQ(this.Theme.ENABLED, true)
          );

          this.theme = await this.client.themeDAO.find(predicate);
          if ( ! this.theme ) {
            console.warn('Theme not found: '+domain);
            this.theme = this.Theme.create({'id':'foam'});
          }
        }
      } catch (err) {
        this.notify(this.LOOK_AND_FEEL_NOT_FOUND, 'error');
        console.error(err);
        return;
      }

      if ( ! lastTheme || lastTheme.id != this.theme.id ) this.useCustomElements();
    },

    function useCustomElements() {
      /** Use custom elements if supplied by the Theme. */
      if ( ! this.theme ) throw new Error(this.LOOK_AND_FEEL_NOT_FOUND);

      if ( this.theme.topNavigation ) {
        this.topNavigation_ = this.theme.topNavigation;
      }

      if ( this.theme.footerView ) {
        this.footerView_ = this.theme.footerView;
      }
    },
    {
      name: 'updateDisplayWidth',
      isMerged: true,
      mergeDelay: 1000,
      code: function() {
        this.displayWidth = foam.u2.layout.DisplayWidth.VALUES
          .concat()
          .sort((a, b) => b.minWidth - a.minWidth)
          .find(o => o.minWidth <= window.innerWidth);
      }
    }
  ]
});
<|endoftext|>


<commit_before>// ==UserScript==
// @name         TruyenCV downloader
// @namespace    http://devs.forumvi.com/
// @description  Tải truyện từ truyencv.com định dạng html. Sau đó, bạn có thể dùng Mobipocket Creator để tạo ebook prc
// @version      2.0.1
// @icon         http://i.imgur.com/o5cmtkU.png
// @author       Zzbaivong
// @license      MIT
// @match        http://truyencv.com/*/
// @require      https://code.jquery.com/jquery-3.2.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.2/handlebars.min.js
// @require      https://greasyfork.org/scripts/20307-jszip-v2/code/jszip-v2.js?version=196156
// @require      https://greasyfork.org/scripts/29904-jszip-utils/code/jszip-utils.js?version=196137
// @require      https://greasyfork.org/scripts/18532-filesaver/code/FileSaver.js?version=164030
// @require      https://greasyfork.org/scripts/29905-js-epub-maker/code/js-epub-maker.js?version=196139
// @noframes
// @connect      self
// @supportURL   https://github.com/baivong/Userscript/issues
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

/* global EpubMaker */
(function ($, window, document) {
    'use strict';

    /**
     * Hiển thị liên kết xem online cuối chương
     * [>]
     * @type {Boolean} true
     *                 false
     */
    var readOnline = true;

    function cleanHtml(str) {
        str = str.replace(/&nbsp\;/gm, ' ');
        str = str.replace(/<(br|hr|img)([^>]+)?>/gm, '<$1$2 />');
        return '
' + str + '
';
    }

    function downloadError(err) {
        $downloadStatus('danger');
        titleError.push(chapTitle);
        console.error(err);
    }

    function saveEbook() {
        if (endDownload) return;
        endDownload = true;

        if (titleError.length) {
            titleError = '
Các chương lỗi: ' + titleError.join(', ') + '
';
        } else {
            titleError = '';
        }
        beginEnd = '
Nội dung từ ' + begin + ' đến ' + end + '
';

        epubMaker.withSection(new EpubMaker.Section('note', 'note', {
            content: beginEnd + titleError + '

' + credits,
            title: 'Ghi chú'
        }, false, true));

        epubMaker.downloadEpub(function (epubZipContent, filename) {
            document.title = '[⇓] ' + ebookTitle;
            $win.off('beforeunload');
            $download.attr({
                href: window.URL.createObjectURL(epubZipContent),
                download: filename
            }).text('Tải xong').off('click');
            $downloadStatus('success');
        });
    }

    function getContent() {
        if (endDownload) return;
        chapId = chapList[count];

        GM_xmlhttpRequest({
            method: 'GET',
            url: pathname + chapId + '/',
            onload: function (response) {
                var $data = $(response.responseText),
                    $chapter = $data.find('#js-truyencv-content'),
                    $notContent = $chapter.find('script, style, a, div, p:last'),
                    $referrer = $chapter.find('[style]').filter(function () {
                        return (this.style.fontSize === '1px' || this.style.fontSize === '0px' || this.style.color === 'white');
                    });

                if (endDownload) return;

                chapTitle = $data.find('#js-truyencv-read-content .title').text().trim();

                if (!$chapter.length) {
                    downloadError('Missing content.');
                } else {
                    $downloadStatus('warning');

                    if ($notContent.length) $notContent.remove();
                    if ($referrer.length) $referrer.remove();

                    var $img = $chapter.find('img');
                    if ($img.length) $img.replaceWith(function () {
                        return '
Click để xem ảnh
';
                    });

                    epubMaker.withSection(new EpubMaker.Section('chapter', chapId, {
                        content: cleanHtml($chapter.html()) + chapRef(referrer + chapId),
                        title: chapTitle
                    }, true, false));

                    if (count === 0) begin = chapTitle;
                    end = chapTitle;

                    $download.html('Đang tải: ' + Math.floor((count / chapListSize) * 100) + '%');

                    count++;
                    document.title = '[' + count + '] ' + pageName;
                    if (count >= chapListSize) {
                        saveEbook();
                    } else {
                        getContent();
                    }
                }
            },
            onerror: function (err) {
                downloadError(err);
                saveEbook();
            }
        });
    }

    function downloadEbook() {
        var $infoBlock = $('.truyencv-detail-info-block'),
            $ebookType = $infoBlock.find('.categories a');

        ebookTitle = $infoBlock.find('h1').text().trim();
        ebookAuthor = $infoBlock.find('.author').text().trim();
        ebookCover = $infoBlock.find('.img-responsive').attr('src');

        if ($ebookType.length) {
            $ebookType.each(function () {
                ebookType.push($(this).text().trim());
            });
            ebookType = ebookType.join(', ');
            ebookType = '
Thể loại: ' + ebookType + '
';
        } else {
            ebookType = '';
        }

        epubMaker = new EpubMaker()
            .withUuid('github.com/baivong/Userscript::truyencv::' + pathname.slice(1, -1))
            .withTemplate('idpf-wasteland')
            .withAuthor(ebookAuthor)
            .withLanguage('vi')
            .withModificationDate(new Date)
            .withCover(ebookCover)
            .withTitle(ebookTitle);

        epubMaker.withSection(new EpubMaker.Section('introduction', 'info', {
            content: '
' + ebookTitle + '

Tác giả: ' + ebookAuthor + '
' + ebookType,
            title: 'Giới thiệu'
        }, false, true));

        epubMaker.withSection(new EpubMaker.Section('preamble', 'content', {
            content: cleanHtml($('#truyencv-detail-introduction .brief').html()),
            title: 'Nội dung'
        }, false, true));

        $win.on('beforeunload', function () {
            return 'Truyện đang được tải xuống...';
        });

        $download.one('click', function (e) {
            e.preventDefault();
            saveEbook();
        });

        getContent();
    }

    var pageName = document.title,
        $win = $(window),
        $download = $('', {
            class: 'btn btn-truyencv',
            href: '#download',
            text: 'Tải xuống'
        }),
        $downloadStatus = function (status) {
            $download.removeClass('btn-truyencv btn-primary btn-success btn-info btn-warning btn-danger').addClass('btn-' + status);
        },

        $novelId = $('.basic'),
        chapList = [],
        chapListSize = 0,
        chapId = '',
        chapTitle = '',
        pathname = location.pathname,
        count = 0,
        begin = '',
        end = '',
        endDownload = false,

        ebookTitle = '',
        ebookAuthor = '',
        ebookCover = '',
        ebookType = [],
        beginEnd = '',
        titleError = [],
        referrer = location.origin + pathname,
        credits = '
Truyện được tải từ TruyenCV

Userscript được viết bởi: Zzbaivong
',
        chapRef = function (ref) {
            return readOnline ? '
[>]
' : '';
        },

        epubMaker;

    if (!$novelId.length) return;

    $download.appendTo('.info .buttons');
    $download.one('click contextmenu', function (e) {
        e.preventDefault();
        var showChapList = $('.truyencv-detail-block a[href="#truyencv-detail-chap"]');

        document.title = '[...] Vui lòng chờ trong giây lát';

        showChapList = showChapList.attr('onclick');
        showChapList = showChapList.match(/\(([^\(\)]+)\)/)[1];
        showChapList = showChapList.match(/[^',]+/g);

        $.post('/index.php', {
            showChapter: 1,
            media_id: showChapList[0],
            number: showChapList[1],
            page: showChapList[2],
            type: showChapList[3]
        }).done(function (data) {
            chapList = data.match(/(?:href\=")[^"\)]+(?=")/g);
            chapList = chapList.reverse();
            chapList = chapList.map(function (val) {
                val = val.slice(6, -1);
                val = val.replace(referrer, '');
                return val;
            });

            if (e.type === 'contextmenu') {
                var startFrom = prompt('Nhập ID chương truyện bắt đầu tải:', chapList[0]);
                startFrom = chapList.indexOf(startFrom);
                if (startFrom !== -1) chapList = chapList.slice(startFrom);
            }

            chapListSize = chapList.length;
            if (chapListSize > 0) downloadEbook();
        }).fail(function (jqXHR, textStatus) {
            downloadError(textStatus);
        });
    });

})(jQuery, window, document);
<commit_msg>Update description<commit_after>// ==UserScript==
// @name         TruyenCV downloader
// @namespace    http://devs.forumvi.com/
// @description  Tải truyện từ truyencv.com định dạng epub
// @version      2.0.2
// @icon         http://i.imgur.com/o5cmtkU.png
// @author       Zzbaivong
// @license      MIT
// @match        http://truyencv.com/*/
// @require      https://code.jquery.com/jquery-3.2.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.2/handlebars.min.js
// @require      https://greasyfork.org/scripts/20307-jszip-v2/code/jszip-v2.js?version=196156
// @require      https://greasyfork.org/scripts/29904-jszip-utils/code/jszip-utils.js?version=196137
// @require      https://greasyfork.org/scripts/18532-filesaver/code/FileSaver.js?version=164030
// @require      https://greasyfork.org/scripts/29905-js-epub-maker/code/js-epub-maker.js?version=196139
// @noframes
// @connect      self
// @supportURL   https://github.com/baivong/Userscript/issues
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

/* global EpubMaker */
(function ($, window, document) {
    'use strict';

    /**
     * Hiển thị liên kết xem online cuối chương
     * [>]
     * @type {Boolean} true
     *                 false
     */
    var readOnline = true;

    function cleanHtml(str) {
        str = str.replace(/&nbsp\;/gm, ' ');
        str = str.replace(/<(br|hr|img)([^>]+)?>/gm, '<$1$2 />');
        return '
' + str + '
';
    }

    function downloadError(err) {
        $downloadStatus('danger');
        titleError.push(chapTitle);
        console.error(err);
    }

    function saveEbook() {
        if (endDownload) return;
        endDownload = true;

        if (titleError.length) {
            titleError = '
Các chương lỗi: ' + titleError.join(', ') + '
';
        } else {
            titleError = '';
        }
        beginEnd = '
Nội dung từ ' + begin + ' đến ' + end + '
';

        epubMaker.withSection(new EpubMaker.Section('note', 'note', {
            content: beginEnd + titleError + '

' + credits,
            title: 'Ghi chú'
        }, false, true));

        epubMaker.downloadEpub(function (epubZipContent, filename) {
            document.title = '[⇓] ' + ebookTitle;
            $win.off('beforeunload');
            $download.attr({
                href: window.URL.createObjectURL(epubZipContent),
                download: filename
            }).text('Tải xong').off('click');
            $downloadStatus('success');
        });
    }

    function getContent() {
        if (endDownload) return;
        chapId = chapList[count];

        GM_xmlhttpRequest({
            method: 'GET',
            url: pathname + chapId + '/',
            onload: function (response) {
                var $data = $(response.responseText),
                    $chapter = $data.find('#js-truyencv-content'),
                    $notContent = $chapter.find('script, style, a, div, p:last'),
                    $referrer = $chapter.find('[style]').filter(function () {
                        return (this.style.fontSize === '1px' || this.style.fontSize === '0px' || this.style.color === 'white');
                    });

                if (endDownload) return;

                chapTitle = $data.find('#js-truyencv-read-content .title').text().trim();

                if (!$chapter.length) {
                    downloadError('Missing content.');
                } else {
                    $downloadStatus('warning');

                    if ($notContent.length) $notContent.remove();
                    if ($referrer.length) $referrer.remove();

                    var $img = $chapter.find('img');
                    if ($img.length) $img.replaceWith(function () {
                        return '
Click để xem ảnh
';
                    });

                    epubMaker.withSection(new EpubMaker.Section('chapter', chapId, {
                        content: cleanHtml($chapter.html()) + chapRef(referrer + chapId),
                        title: chapTitle
                    }, true, false));

                    if (count === 0) begin = chapTitle;
                    end = chapTitle;

                    $download.html('Đang tải: ' + Math.floor((count / chapListSize) * 100) + '%');

                    count++;
                    document.title = '[' + count + '] ' + pageName;
                    if (count >= chapListSize) {
                        saveEbook();
                    } else {
                        getContent();
                    }
                }
            },
            onerror: function (err) {
                downloadError(err);
                saveEbook();
            }
        });
    }

    function downloadEbook() {
        var $infoBlock = $('.truyencv-detail-info-block'),
            $ebookType = $infoBlock.find('.categories a');

        ebookTitle = $infoBlock.find('h1').text().trim();
        ebookAuthor = $infoBlock.find('.author').text().trim();
        ebookCover = $infoBlock.find('.img-responsive').attr('src');

        if ($ebookType.length) {
            $ebookType.each(function () {
                ebookType.push($(this).text().trim());
            });
            ebookType = ebookType.join(', ');
            ebookType = '
Thể loại: ' + ebookType + '
';
        } else {
            ebookType = '';
        }

        epubMaker = new EpubMaker()
            .withUuid('github.com/baivong/Userscript::truyencv::' + pathname.slice(1, -1))
            .withTemplate('idpf-wasteland')
            .withAuthor(ebookAuthor)
            .withLanguage('vi')
            .withModificationDate(new Date)
            .withCover(ebookCover)
            .withTitle(ebookTitle);

        epubMaker.withSection(new EpubMaker.Section('introduction', 'info', {
            content: '
' + ebookTitle + '

Tác giả: ' + ebookAuthor + '
' + ebookType,
            title: 'Giới thiệu'
        }, false, true));

        epubMaker.withSection(new EpubMaker.Section('preamble', 'content', {
            content: cleanHtml($('#truyencv-detail-introduction .brief').html()),
            title: 'Nội dung'
        }, false, true));

        $win.on('beforeunload', function () {
            return 'Truyện đang được tải xuống...';
        });

        $download.one('click', function (e) {
            e.preventDefault();
            saveEbook();
        });

        getContent();
    }

    var pageName = document.title,
        $win = $(window),
        $download = $('', {
            class: 'btn btn-truyencv',
            href: '#download',
            text: 'Tải xuống'
        }),
        $downloadStatus = function (status) {
            $download.removeClass('btn-truyencv btn-primary btn-success btn-info btn-warning btn-danger').addClass('btn-' + status);
        },

        $novelId = $('.basic'),
        chapList = [],
        chapListSize = 0,
        chapId = '',
        chapTitle = '',
        pathname = location.pathname,
        count = 0,
        begin = '',
        end = '',
        endDownload = false,

        ebookTitle = '',
        ebookAuthor = '',
        ebookCover = '',
        ebookType = [],
        beginEnd = '',
        titleError = [],
        referrer = location.origin + pathname,
        credits = '
Truyện được tải từ TruyenCV

Userscript được viết bởi: Zzbaivong
',
        chapRef = function (ref) {
            return readOnline ? '
[>]
' : '';
        },

        epubMaker;

    if (!$novelId.length) return;

    $download.appendTo('.info .buttons');
    $download.one('click contextmenu', function (e) {
        e.preventDefault();
        var showChapList = $('.truyencv-detail-block a[href="#truyencv-detail-chap"]');

        document.title = '[...] Vui lòng chờ trong giây lát';

        showChapList = showChapList.attr('onclick');
        showChapList = showChapList.match(/\(([^\(\)]+)\)/)[1];
        showChapList = showChapList.match(/[^',]+/g);

        $.post('/index.php', {
            showChapter: 1,
            media_id: showChapList[0],
            number: showChapList[1],
            page: showChapList[2],
            type: showChapList[3]
        }).done(function (data) {
            chapList = data.match(/(?:href\=")[^"\)]+(?=")/g);
            chapList = chapList.reverse();
            chapList = chapList.map(function (val) {
                val = val.slice(6, -1);
                val = val.replace(referrer, '');
                return val;
            });

            if (e.type === 'contextmenu') {
                var startFrom = prompt('Nhập ID chương truyện bắt đầu tải:', chapList[0]);
                startFrom = chapList.indexOf(startFrom);
                if (startFrom !== -1) chapList = chapList.slice(startFrom);
            }

            chapListSize = chapList.length;
            if (chapListSize > 0) downloadEbook();
        }).fail(function (jqXHR, textStatus) {
            downloadError(textStatus);
        });
    });

})(jQuery, window, document);
<|endoftext|>


<commit_before>// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var symbolAltKeys = {
  '1': ['\u00B9', // Superscript 1
        '\u00BD', // Vulgar fraction 1/2
        '\u2153', // Vulgar fraction 1/3
        '\u00BC', // Vulgar fraction 1/4
        '\u215B'], // Vulgar fraction 1/8
  '2': ['\u00B2', // Superscript 2
        '\u2154'], // Vulgar fraction 2/3
  '3': ['\u00B3', // Superscript 3
        '\u00BE', // Vulgar fraction 3/4
        '\u215C'], // Vulgar fraction 3/8
  '4': ['\u2074'], // Superscript 4
  '5': ['\u215D'], // Vulgar fraction 5/8
  '7': ['\u215E'], // Vulgar fraction 7/8
  '0': ['\u00D8', // Empty set
        '\u207F'], // Superscript small n
  '$': ['\u20AC', // Euro sign
        '\u00A5', // Yen sign
        '\u00A3', // Pound sign
        '\u00A2'], // Cent sign
  '%': ['\u2030'], // Per Mille sign
  '*': ['\u2605', // Black star
        '\u2020', // Dagger
        '\u2021'], // Double dagger
  '\\': ['/'],
  '"': ['\u201C', // Left double quote
        '\u201D', // Right double quote
        '\u201E', // Double low-9 quotation mark
        '\u00AB', // Left double angle quote
        '\u00BB'], // Right double angle quote
  '-': ['_'],
  '!': ['\u00A1'], // Inverted exclamation mark
  '?': ['\u00BF'], // Inverted question mark
  '(': ['<', '{', '['],
  ')': ['>', '}', ']']
};

document.addEventListener('WebComponentsReady', function() {
  var altkeyMetadata = document.createElement('kb-altkey-data');
  altkeyMetadata.registerAltkeys(symbolAltKeys);
});
<commit_msg>Added .gov, .net, .org as alternates to .com. BUG=259345<commit_after>// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var symbolAltKeys = {
  '1': ['\u00B9', // Superscript 1
        '\u00BD', // Vulgar fraction 1/2
        '\u2153', // Vulgar fraction 1/3
        '\u00BC', // Vulgar fraction 1/4
        '\u215B'], // Vulgar fraction 1/8
  '2': ['\u00B2', // Superscript 2
        '\u2154'], // Vulgar fraction 2/3
  '3': ['\u00B3', // Superscript 3
        '\u00BE', // Vulgar fraction 3/4
        '\u215C'], // Vulgar fraction 3/8
  '4': ['\u2074'], // Superscript 4
  '5': ['\u215D'], // Vulgar fraction 5/8
  '7': ['\u215E'], // Vulgar fraction 7/8
  '0': ['\u00D8', // Empty set
        '\u207F'], // Superscript small n
  '$': ['\u20AC', // Euro sign
        '\u00A5', // Yen sign
        '\u00A3', // Pound sign
        '\u00A2'], // Cent sign
  '%': ['\u2030'], // Per Mille sign
  '*': ['\u2605', // Black star
        '\u2020', // Dagger
        '\u2021'], // Double dagger
  '\\': ['/'],
  '"': ['\u201C', // Left double quote
        '\u201D', // Right double quote
        '\u201E', // Double low-9 quotation mark
        '\u00AB', // Left double angle quote
        '\u00BB'], // Right double angle quote
  '-': ['_'],
  '!': ['\u00A1'], // Inverted exclamation mark
  '?': ['\u00BF'], // Inverted question mark
  '(': ['<', '{', '['],
  ')': ['>', '}', ']'],
  '.com' : ['.net','.org','.gov']
};

document.addEventListener('WebComponentsReady', function() {
  var altkeyMetadata = document.createElement('kb-altkey-data');
  altkeyMetadata.registerAltkeys(symbolAltKeys);
});
<|endoftext|>


<commit_before>import { h, defineComponent } from 'vue'

import { hMergeSlot } from '../../utils/render.js'

import QIcon from '../icon/QIcon.js'

import RouterLinkMixin from '../../mixins/router-link.js'

export default defineComponent({
  name: 'QBreadcrumbsEl',

  mixins: [RouterLinkMixin],

  props: {
    label: String,
    icon: String,

    tag: {
      type: String,
      default: 'span'
    }
  },

  render () {
    const child = []

    this.icon !== void 0 && child.push(
      h(QIcon, {
        class: 'q-breadcrumbs__el-icon' +
          (this.label !== void 0 ? ' q-breadcrumbs__el-icon--with-label' : ''),
        name: this.icon
      })
    )

    this.label !== void 0 && child.push(this.label)

    return h(this.linkTag, {
      class: 'q-breadcrumbs__el q-link flex inline items-center relative-position',
      ...this.linkProps
    }, hMergeSlot(this, 'default', child))
  }
})
<commit_msg>feat(QBreadcrumbsEl): composition api<commit_after>import { h, defineComponent, getCurrentInstance } from 'vue'

import QIcon from '../icon/QIcon.js'

import { hMergeSlot } from '../../utils/composition-render.js'
import useRouterLink, { useRouterLinkProps } from '../../composables/use-router-link.js'

export default defineComponent({
  name: 'QBreadcrumbsEl',

  props: {
    ...useRouterLinkProps,

    label: String,
    icon: String,

    tag: {
      type: String,
      default: 'span'
    }
  },

  setup (props, { slots, attrs }) {
    const vm = getCurrentInstance()
    const { linkTag, linkProps } = useRouterLink(props, vm, attrs)

    return () => {
      const child = []

      props.icon !== void 0 && child.push(
        h(QIcon, {
          class: 'q-breadcrumbs__el-icon' +
            (props.label !== void 0 ? ' q-breadcrumbs__el-icon--with-label' : ''),
          name: props.icon
        })
      )

      props.label !== void 0 && child.push(props.label)

      return h(linkTag.value, {
        class: 'q-breadcrumbs__el q-link flex inline items-center relative-position',
        ...linkProps.value
      }, hMergeSlot(slots.default, child))
    }
  }
})
<|endoftext|>