<commit_before>
// SmoothScroll v0.9.4
// Licensed under the terms of the MIT license.

// People involved
// - Balazs Galambosi: maintainer (CHANGELOG.txt)
// - Patrick Brunner (patrickb1991@gmail.com)
// - Michael Herf: Pulse Algorithm

// Frame Variables
var frame         = false;
var noscrollframe = false;

// Scroll Variables (tweakable)
var framerate = 150; // [Hz]
var animtime  = 400; // [px]
var stepsize  = 120; // [px]

// Pulse (less tweakable)
// ratio of "tail" to "acceleration"
var pulseAlgorithm = true;
var pulseScale     = 8;
var pulseNormalize = 1;

// Keyboard Settings
var keyboardsupport = true;
var disableKeyboard = false;
var arrowscroll     = 50; // [px]

// Excluded pages
var exclude = "";

// Other Variables
var scrolls;
var deltaX = 0;
var deltaY = 0;
var direction = "down";
var initdone  = false;
var activeElement;

var key = { up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 };

/***********************************************
 * SETTINGS
 ***********************************************/

chrome.extension.connect({ name: "smoothscroll" }).
onMessage.addListener(function (settings) {

    // NOTE: + converts to {Number}
    framerate = Math.min(+settings.framerate, 250);
    animtime  = +settings.animtime;
    stepsize  = +settings.scrollsz;
    exclude   = settings.exclude;
    pulseAlgorithm  = (settings.pulseAlgorithm == "true");
    pulseScale      = +settings.pulseScale;
    keyboardsupport = (settings.keyboardsupport == "true");
    arrowscroll     = +settings.arrscroll;

    computePulseScale();

    // it seems that sometimes settings come late
    // and we need to test again for excluded pages
    initTest();

    if (keyboardsupport && !disableKeyboard) {
        addEvent("keydown", keydown);
    }

    // If extension settings were deleted somehow
    if (!framerate) {
        alert("SmoothScroll: Please restart Chrome");
    }
});

/***********************************************
 * INITIALIZE
 ***********************************************/

/**
 * Tests if smooth scrolling is allowed. Shuts down everything if not.
 */
function initTest() {

    // disable keys for google reader (spacebar conflict)
    if (document.URL.indexOf("google.com/reader/view") > -1) {
        disableKeyboard = true;
    }

    // disable everything if the page is blacklisted
    if (exclude) {
        var domains = exclude.split(/[,\n] ?/);
        for (var i = domains.length; i--;) {
            if (document.URL.indexOf(domains[i]) > -1) {
                removeEvent("mousewheel", wheel);
                disableKeyboard = true;
                break;
            }
        }
    }

    // disable keyboard support if anything above requested it
    if (disableKeyboard) {
        removeEvent("keydown", keydown);
    }
}

/**
 * Sets up scrolls array, determines if frames are involved.
 */
function init() {

    var body  = document.body;
    var docel = document.documentElement;
    var htmlHeight   = docel.offsetHeight;
    var windowHeight = window.innerHeight;

    activeElement = body;

    initTest();

    // Checks if this script is running in a frame
    if (top && top != self) {
        frame = true;
        if (docel.scrollHeight <= docel.clientHeight + 10) {
            noscrollframe = true;
        }
    }

    /**
     * This fixes a bug where the areas left and right to
     * the content does not trigger the onmousewheel event
     * on some pages. e.g.: html, body { height: 100% }
     */
    else if (body.scrollHeight > windowHeight &&
            (body.offsetHeight <= windowHeight || htmlHeight <= windowHeight)) {

        var underlay = document.createElement("div");
        underlay.setAttribute( "style",
            "z-index: -1; position: absolute; top: 0; left: 0; " +
            "width: 100%; height: " + body.scrollHeight + "px;" );
        body.appendChild(underlay);

        var scrollHeight = body.scrollHeight;
        var pending = false;

        // DOMChange delayed
        var refresh = function() {
            if (!pending && scrollHeight != body.scrollHeight) {
                pending = true; // add a new pending action
                setTimeout(function(){
                    underlay.style.height = body.scrollHeight + "px";
                    scrollHeight = body.scrollHeight;
                    pending = false;
                }, 1000); // act rarely to stay fast
            }
        }

        addEvent("DOMNodeInserted", refresh);
        addEvent("DOMNodeRemoved",  refresh);
    }
    initdone = true;
}

/************************************************
 * SCROLLING
 ************************************************/

var que = [];
var pending = false;

/**
 * Pushes scroll actions to a given direction Array.
 */
function scrollArray(elem, dir, multiplyX, multiplyY, delay) {

    delay || (delay = 1000);
    directionCheck(dir);

    // que contains the scroll commands [start, x, y]
    que.push([multiplyX, multiplyY, +new Date, 0]); // [x, y, start, lastpos]

    function step() {

        var now = +new Date;
        var addX = 0;
        var addY = 0;
        var finished;
        var x = 0, y = 1, start = 2, last = 3; // {enum}

        for (var i = 0; i < que.length; i++) {

            var item = que[i];
            var elapsed = now - item[start];
            finished = (elapsed >= animtime);

            // scroll is [0, 1]
            var scroll = finished ? 1 : elapsed / animtime;
            // easing
            if (pulseAlgorithm) {
                scroll = pulse(scroll);
            }

            // scale and quantize to int so our pixel difference works:
            var iscroll = Math.floor(stepsize * scroll + 0.99);
            scroll = iscroll - item[last];
            item[last] = iscroll;

            // add this to the actual scrolling
            addX += item[x] * scroll;
            addY += item[y] * scroll;

            // delete and step back
            if (finished) {
                que.splice(i, 1); i--;
                continue;
            }
        }

        addX = addX >> 0; // toInt
        addY = addY >> 0; // toInt

        // scroll left
        if (multiplyX && addX) {
            var lastLeft = elem.scrollLeft;
            elem.scrollLeft += addX;

            // scroll left failed (edge)
            if (elem.scrollLeft === lastLeft) {
                multiplyX = 0;
            }
        }

        // scroll top
        if (multiplyY && addY) {
            var lastTop = elem.scrollTop;
            elem.scrollTop += addY;

            // scroll top failed (edge)
            if (elem.scrollTop === lastTop) {
                multiplyY = 0;
            }
        }

        // clean up if there's nothing left to do
        if (!multiplyX && !multiplyY) {
            que = [];
        }

        if (que.length) {
            setTimeout(step, delay / framerate + 1);
        } else {
            pending = false;
        }
    }

    // start a new que of actions
    if (!pending) {
        setTimeout(step, 0);
        pending = true;
    }
}

/***********************************************
 * EVENTS
 ***********************************************/

/**
 * Mouse wheel handler.
 * NOTE: function logic should be decomposed.
 * @param {Object} event
 */
function wheel(event) {

    if (!initdone) {
        init();
    }

    var target = event.target;

    // use default if there's no overflowing
    // element or default action is prevented
    var elem = overflowingAncestor(target);
    if (!elem || noscrollframe || event.defaultPrevented ||
        isNodeName(activeElement, "embed") ||
       (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
        return true;
    }

    deltaX = event.wheelDeltaX || 0;
    deltaY = event.wheelDeltaY || 0;

    // normalize deltas
    // synaptics seems to send 1 sometimes,
    // and 120 other times (fix)
    if (Math.abs(deltaX) > 1.2) {
        deltaX /= 120;
    }
    if (Math.abs(deltaY) > 1.2) {
        deltaY /= 120;
    }

    var dir = (deltaY > 0) ? "up" : "down";
    scrollArray(elem, dir, -deltaX, -deltaY);
    event.preventDefault();
}

/**
 * Keydown event handler.
 * @param {Object} event
 */
function keydown(event) {

    var target   = event.target;
    var modifier = event.ctrlKey || event.altKey || event.metaKey;

    // do nothing if user is editing text
    // or using a modifier key (except shift)
    if ( /input|textarea|embed/i.test(target.nodeName) ||
         target.isContentEditable ||
         event.defaultPrevented   ||
         modifier ) {
      return true;
    }
    // spacebar should trigger button press
    if (isNodeName(target, "button") &&
        event.keyCode === key.spacebar) {
      return true;
    }

    var scale, dir, shift;
    var elem = overflowingAncestor(activeElement);
    var clientHeight = elem.clientHeight;

    if (elem == document.body) {
        clientHeight = window.innerHeight;
    }

    switch (event.keyCode) {
        case key.up:
            scale = -arrowscroll;
            dir = "up";
            break;
        case key.down:
            scale = arrowscroll;
            dir = "down";
            break;
        case key.spacebar: // (+ shift)
            shift = event.shiftKey ? 1 : -1;
            scale = -shift * clientHeight * 0.9;
            dir = (shift > 0) ? "up" : "down";
            break;
        case key.pageup:
            scale = -clientHeight * 0.9;
            dir = "up";
            break;
        case key.pagedown:
            scale = clientHeight * 0.9;
            dir = "down";
            break;
        case key.home:
            scale = -elem.scrollTop;
            dir = "up";
            break;
        case key.end:
            var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
            scale = (damt > 0) ? damt+10 : 0;
            dir = "down";
            break;
        default:
            return true; // a key we don"t care about
    }
    scale /= stepsize;
    scrollArray(elem, dir, 0, scale);
    event.preventDefault();
}

/**
 * Mousedown event only for updating activeElement
 */
function mousedown(event) {
    activeElement = event.target;
}

/***********************************************
 * HELPERS
 ***********************************************/

function addEvent(type, fn, bubble) {
    window.addEventListener(type, fn, (bubble||false));
}

function removeEvent(type, fn, bubble) {
    window.removeEventListener(type, fn, (bubble||false));
}

function isNodeName(el, tag) {
    return el.nodeName.toLowerCase() === tag.toLowerCase();
}

function directionCheck(dir) {
    if (direction !== dir) {
        que = [];
        direction = dir;
    }
}

function overflowingAncestor(el) {
    var bodyScrollHeight = document.body.scrollHeight;
    do {
        if (bodyScrollHeight === el.scrollHeight) {
            return document.body;
        }
        else if (el.clientHeight + 10 < el.scrollHeight) {
            overflow = getComputedStyle(el, "").getPropertyValue("overflow");
            if (overflow === "scroll" || overflow === "auto") {
                return el;
            }
        }
    } while (el = el.parentNode);
}

/***********************************************
 * PULSE
 ***********************************************/

/**
 * Viscous fluid with a pulse for part and decay for the rest.
 * - Applies a fixed force over an interval (a damped acceleration), and
 * - Lets the exponential bleed away the velocity over a longer interval
 * - Michael Herf, http://stereopsis.com/stopping/
 */
function pulse_(x) {
    var val, start, expx;
    // test
    x = x * pulseScale;
    if (x < 1) { // acceleartion
        val = x - (1 - Math.exp(-x));
    } else {     // tail
        // the previous animation ended here:
        start = Math.exp(-1);
        // simple viscous drag
        x -= 1;
        expx = 1 - Math.exp(-x);
        val = start + (expx * (1 - start));
    }
    return val * pulseNormalize;
}

function computePulseScale() {
    pulseNormalize = 1 / pulse_(1);
}

/**
 * Viscous fluid with a pulse for part and decay for the rest
 */
function pulse(x) {
    if (x >= 1) return 1;
    if (x <= 0) return 0;

    if (pulseNormalize == 1) {
        computePulseScale();
    }
    return pulse_(x);
}

addEvent("mousedown", mousedown);
addEvent("mousewheel", wheel);
addEvent("DOMContentLoaded", init);
<commit_msg>minor changes<commit_after>
// SmoothScroll v0.9.4
// Licensed under the terms of the MIT license.

// People involved
// - Balazs Galambosi: maintainer (CHANGELOG.txt)
// - Patrick Brunner (patrickb1991@gmail.com)
// - Michael Herf: Pulse Algorithm

// Frame Variables
var frame         = false;
var noscrollframe = false;

// Scroll Variables (tweakable)
var framerate = 150; // [Hz]
var animtime  = 400; // [px]
var stepsize  = 120; // [px]

// Pulse (less tweakable)
// ratio of "tail" to "acceleration"
var pulseAlgorithm = true;
var pulseScale     = 8;
var pulseNormalize = 1;

// Keyboard Settings
var keyboardsupport = true;
var disableKeyboard = false;
var arrowscroll     = 50; // [px]

// Excluded pages
var exclude = "";

// Other Variables
var scrolls;
var deltaX = 0;
var deltaY = 0;
var direction = "down";
var initdone  = false;
var activeElement;

var key = { up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 };

/***********************************************
 * SETTINGS
 ***********************************************/

chrome.extension.connect({ name: "smoothscroll" }).
onMessage.addListener(function (settings) {

    // NOTE: + converts to {Number}
    framerate = Math.min(+settings.framerate, 250);
    animtime  = +settings.animtime;
    stepsize  = +settings.scrollsz;
    exclude   = settings.exclude;
    pulseAlgorithm  = (settings.pulseAlgorithm == "true");
    pulseScale      = +settings.pulseScale;
    keyboardsupport = (settings.keyboardsupport == "true");
    arrowscroll     = +settings.arrscroll;

    computePulseScale();

    // it seems that sometimes settings come late
    // and we need to test again for excluded pages
    initTest();

    if (keyboardsupport && !disableKeyboard) {
        addEvent("keydown", keydown);
    }

    // If extension settings were deleted somehow
    if (!framerate) {
        alert("SmoothScroll: Please restart Chrome");
    }
});

/***********************************************
 * INITIALIZE
 ***********************************************/

/**
 * Tests if smooth scrolling is allowed. Shuts down everything if not.
 */
function initTest() {

    // disable keys for google reader (spacebar conflict)
    if (document.URL.indexOf("google.com/reader/view") > -1) {
        disableKeyboard = true;
    }

    // disable everything if the page is blacklisted
    if (exclude) {
        var domains = exclude.split(/[,\n] ?/);
        for (var i = domains.length; i--;) {
            if (document.URL.indexOf(domains[i]) > -1) {
                removeEvent("mousewheel", wheel);
                disableKeyboard = true;
                break;
            }
        }
    }

    // disable keyboard support if anything above requested it
    if (disableKeyboard) {
        removeEvent("keydown", keydown);
    }
}

/**
 * Sets up scrolls array, determines if frames are involved.
 */
function init() {

    var body  = document.body;
    var docel = document.documentElement;
    var htmlHeight   = docel.offsetHeight;
    var windowHeight = window.innerHeight;

    activeElement = body;

    initTest();

    // Checks if this script is running in a frame
    if (top && top != self) {
        frame = true;
        if (docel.scrollHeight <= docel.clientHeight + 10) {
            noscrollframe = true;
        }
    }

    /**
     * This fixes a bug where the areas left and right to
     * the content does not trigger the onmousewheel event
     * on some pages. e.g.: html, body { height: 100% }
     */
    else if (body.scrollHeight > windowHeight &&
            (body.offsetHeight <= windowHeight || htmlHeight <= windowHeight)) {

        var underlay = document.createElement("div");
        underlay.setAttribute( "style",
            "z-index: -1; position: absolute; top: 0; left: 0; " +
            "width: 100%; height: " + body.scrollHeight + "px;" );
        body.appendChild(underlay);

        var scrollHeight = body.scrollHeight;
        var pending = false;

        // DOMChange delayed
        var refresh = function() {
            if (!pending && scrollHeight != body.scrollHeight) {
                pending = true; // add a new pending action
                setTimeout(function(){
                    underlay.style.height = body.scrollHeight + "px";
                    scrollHeight = body.scrollHeight;
                    pending = false;
                }, 1000); // act rarely to stay fast
            }
        }

        addEvent("DOMNodeInserted", refresh);
        addEvent("DOMNodeRemoved",  refresh);
    }
    initdone = true;
}

/************************************************
 * SCROLLING
 ************************************************/

var que = [];
var pending = false;

/**
 * Pushes scroll actions to a given direction Array.
 */
function scrollArray(elem, dir, multiplyX, multiplyY, delay) {

    delay || (delay = 1000);
    directionCheck(dir);

    // contains the scroll commands [ x, y, start, last ]
    que.push([multiplyX, multiplyY, +new Date, 0]);

    function step() {

        var now = +new Date;
        var addX = 0;
        var addY = 0;
        var finished;
        var x = 0, y = 1, start = 2, last = 3; // {enum}

        for (var i = 0; i < que.length; i++) {

            var item = que[i];
            var elapsed = now - item[start];
            finished = (elapsed >= animtime);

            // scroll is [0, 1]
            var scroll = (finished) ? 1 : elapsed / animtime;
            // easing
            if (pulseAlgorithm) {
                scroll = pulse(scroll);
            }

            // scale and quantize to int so our pixel difference works:
            var iscroll = Math.floor(stepsize * scroll + 0.99);
            scroll = iscroll - item[last];
            item[last] = iscroll;

            // add this to the actual scrolling
            addX += item[x] * scroll;
            addY += item[y] * scroll;

            // delete and step back
            if (finished) {
                que.splice(i, 1); i--;
            }
        }

        addX = addX >> 0; // toInt
        addY = addY >> 0; // toInt

        // scroll left
        if (multiplyX && addX) {
            var lastLeft = elem.scrollLeft;
            elem.scrollLeft += addX;

            // scroll left failed (edge)
            if (elem.scrollLeft === lastLeft) {
                multiplyX = 0;
            }
        }

        // scroll top
        if (multiplyY && addY) {
            var lastTop = elem.scrollTop;
            elem.scrollTop += addY;

            // scroll top failed (edge)
            if (elem.scrollTop === lastTop) {
                multiplyY = 0;
            }
        }

        // clean up if there's nothing left to do
        if (!multiplyX && !multiplyY) {
            que = [];
        }

        if (que.length) {
            setTimeout(step, delay / framerate + 1);
        } else {
            pending = false;
        }
    }

    // start a new que of actions
    if (!pending) {
        setTimeout(step, 0);
        pending = true;
    }
}

/***********************************************
 * EVENTS
 ***********************************************/

/**
 * Mouse wheel handler.
 * NOTE: function logic should be decomposed.
 * @param {Object} event
 */
function wheel(event) {

    if (!initdone) {
        init();
    }

    var target = event.target;

    // use default if there's no overflowing
    // element or default action is prevented
    var elem = overflowingAncestor(target);
    if (!elem || noscrollframe || event.defaultPrevented ||
        isNodeName(activeElement, "embed") ||
       (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
        return true;
    }

    deltaX = event.wheelDeltaX || 0;
    deltaY = event.wheelDeltaY || 0;

    // normalize deltas
    // synaptics seems to send 1 sometimes,
    // and 120 other times (fix)
    if (Math.abs(deltaX) > 1.2) {
        deltaX /= 120;
    }
    if (Math.abs(deltaY) > 1.2) {
        deltaY /= 120;
    }

    var dir = (deltaY > 0) ? "up" : "down";
    scrollArray(elem, dir, -deltaX, -deltaY);
    event.preventDefault();
}

/**
 * Keydown event handler.
 * @param {Object} event
 */
function keydown(event) {

    var target   = event.target;
    var modifier = event.ctrlKey || event.altKey || event.metaKey;

    // do nothing if user is editing text
    // or using a modifier key (except shift)
    if ( /input|textarea|embed/i.test(target.nodeName) ||
         target.isContentEditable ||
         event.defaultPrevented   ||
         modifier ) {
      return true;
    }
    // spacebar should trigger button press
    if (isNodeName(target, "button") &&
        event.keyCode === key.spacebar) {
      return true;
    }

    var scale, dir, shift;
    var elem = overflowingAncestor(activeElement);
    var clientHeight = elem.clientHeight;

    if (elem == document.body) {
        clientHeight = window.innerHeight;
    }

    switch (event.keyCode) {
        case key.up:
            scale = -arrowscroll;
            dir = "up";
            break;
        case key.down:
            scale = arrowscroll;
            dir = "down";
            break;
        case key.spacebar: // (+ shift)
            shift = event.shiftKey ? 1 : -1;
            scale = -shift * clientHeight * 0.9;
            dir = (shift > 0) ? "up" : "down";
            break;
        case key.pageup:
            scale = -clientHeight * 0.9;
            dir = "up";
            break;
        case key.pagedown:
            scale = clientHeight * 0.9;
            dir = "down";
            break;
        case key.home:
            scale = -elem.scrollTop;
            dir = "up";
            break;
        case key.end:
            var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
            scale = (damt > 0) ? damt+10 : 0;
            dir = "down";
            break;
        default:
            return true; // a key we don"t care about
    }
    scale /= stepsize;
    scrollArray(elem, dir, 0, scale);
    event.preventDefault();
}

/**
 * Mousedown event only for updating activeElement
 */
function mousedown(event) {
    activeElement = event.target;
}

/***********************************************
 * HELPERS
 ***********************************************/

function addEvent(type, fn, bubble) {
    window.addEventListener(type, fn, (bubble||false));
}

function removeEvent(type, fn, bubble) {
    window.removeEventListener(type, fn, (bubble||false));
}

function isNodeName(el, tag) {
    return el.nodeName.toLowerCase() === tag.toLowerCase();
}

function directionCheck(dir) {
    if (direction !== dir) {
        que = [];
        direction = dir;
    }
}

function overflowingAncestor(el) {
    var bodyScrollHeight = document.body.scrollHeight;
    do {
        if (bodyScrollHeight === el.scrollHeight) {
            return document.body;
        }
        else if (el.clientHeight + 10 < el.scrollHeight) {
            overflow = getComputedStyle(el, "").getPropertyValue("overflow");
            if (overflow === "scroll" || overflow === "auto") {
                return el;
            }
        }
    } while (el = el.parentNode);
}

/***********************************************
 * PULSE
 ***********************************************/

/**
 * Viscous fluid with a pulse for part and decay for the rest.
 * - Applies a fixed force over an interval (a damped acceleration), and
 * - Lets the exponential bleed away the velocity over a longer interval
 * - Michael Herf, http://stereopsis.com/stopping/
 */
function pulse_(x) {
    var val, start, expx;
    // test
    x = x * pulseScale;
    if (x < 1) { // acceleartion
        val = x - (1 - Math.exp(-x));
    } else {     // tail
        // the previous animation ended here:
        start = Math.exp(-1);
        // simple viscous drag
        x -= 1;
        expx = 1 - Math.exp(-x);
        val = start + (expx * (1 - start));
    }
    return val * pulseNormalize;
}

function computePulseScale() {
    pulseNormalize = 1 / pulse_(1);
}

/**
 * Viscous fluid with a pulse for part and decay for the rest
 */
function pulse(x) {
    if (x >= 1) return 1;
    if (x <= 0) return 0;

    if (pulseNormalize == 1) {
        computePulseScale();
    }
    return pulse_(x);
}

addEvent("mousedown", mousedown);
addEvent("mousewheel", wheel);
addEvent("DOMContentLoaded", init);
<|endoftext|>


<commit_before>
// SmoothScroll v0.9.4
// Licensed under the terms of the MIT license.

// People involved
// - Balazs Galambosi: maintainer (CHANGELOG.txt)
// - Patrick Brunner (patrickb1991@gmail.com)
// - Michael Herf: Pulse Algorithm

// Frame Variables
var frame         = false;
var noscrollframe = false;

// Scroll Variables (tweakable)
var framerate = 150; // [Hz]
var animtime  = 400; // [px]
var stepsize  = 120; // [px]

// Pulse (less tweakable)
// ratio of "tail" to "acceleration"
var pulseAlgorithm = true;
var pulseScale     = 8;
var pulseNormalize = 1;

// Keyboard Settings
var keyboardsupport = true;
var disableKeyboard = false;
var arrowscroll     = 50; // [px]

// Excluded pages
var exclude = "";

// Other Variables
var scrolls;
var deltaX = 0;
var deltaY = 0;
var direction = "down";
var initdone  = false;
var activeElement;

var key = { up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 };

/***********************************************
 * SETTINGS
 ***********************************************/

chrome.extension.connect({ name: "smoothscroll" }).
onMessage.addListener(function (settings) {

    // NOTE: + converts to {Number}
    framerate = Math.min(+settings.framerate, 250);
    animtime  = +settings.animtime;
    stepsize  = +settings.scrollsz;
    exclude   = settings.exclude;
    pulseAlgorithm  = (settings.pulseAlgorithm == "true");
    pulseScale      = +settings.pulseScale;
    keyboardsupport = (settings.keyboardsupport == "true");
    arrowscroll     = +settings.arrscroll;

    scrolls = setupScrolls();
    computePulseScale();

    // it seems that sometimes settings come late
    // and we need to test again for excluded pages
    initTest();

    if (keyboardsupport && !disableKeyboard) {
        addEvent("keydown", keydown);
    }

    // If extension settings were deleted somehow
    if (!framerate) {
        alert("SmoothScroll: Please restart Chrome");
    }
});

/***********************************************
 * INITIALIZE
 ***********************************************/

/**
 * Tests if smooth scrolling is allowed. Shuts down everything if not.
 */
function initTest() {

    // disable keys for google reader (spacebar conflict)
    if (document.URL.indexOf("google.com/reader/view") > -1) {
        disableKeyboard = true;
    }

    // disable everything if the page is blacklisted
    if (exclude) {
        var domains = exclude.split(/[,\n] ?/);
        for (var i = domains.length; i--;) {
            if (document.URL.indexOf(domains[i]) > -1) {
                removeEvent("mousewheel", wheel);
                disableKeyboard = true;
                break;
            }
        }
    }

    // disable keyboard support if anything above requested it
    if (disableKeyboard) {
        removeEvent("keydown", keydown);
    }
}

/**
 * Fills up the scrolls array with easing values.
 * Uses pulse to make it smooth.
 */
function setupScrolls() {

    scrolls = [];

    var last = 0;
    var frames = Math.floor(framerate * animtime / 1000);

    for (var i = 0; i < frames; i++) {
        // scroll is [0, 1]
        var scroll = (i + 1) / frames;
        // transform [0, 1] -> [0, 1]:
        if (pulseAlgorithm) {
           scroll = pulse(scroll);
        }

        // scale and quantize to int so our pixel difference works:
        var iscroll = Math.floor(stepsize * scroll + 0.99);
        scrolls.push(iscroll - last);
        last = iscroll;
    }

    return scrolls;
}

/**
 * Sets up scrolls array, determines if frames are involved.
 */
function init() {

    var body  = document.body;
    var docel = document.documentElement;
    var htmlHeight   = docel.offsetHeight;
    var windowHeight = window.innerHeight;

    activeElement = body;

    initTest();

    if (!scrolls) {
      scrolls = setupScrolls();
    }

    // Checks if this script is running in a frame
    if (top && top != self) {
        frame = true;
        if (docel.scrollHeight <= docel.clientHeight + 10) {
            noscrollframe = true;
        }
    }

    /**
     * This fixes a bug where the areas left and right to
     * the content does not trigger the onmousewheel event
     * on some pages. e.g.: html, body { height: 100% }
     */
    else if (body.scrollHeight > windowHeight &&
            (body.offsetHeight <= windowHeight || htmlHeight <= windowHeight)) {

        var underlay = document.createElement("div");
        underlay.setAttribute( "style",
            "z-index: -1; position: absolute; top: 0; left: 0; " +
            "width: 100%; height: " + body.scrollHeight + "px;" );
        body.appendChild(underlay);

        var scrollHeight = body.scrollHeight;
        var pending = false;

        // DOMChange delayed
        var refresh = function() {
            if (!pending && scrollHeight != body.scrollHeight) {
                pending = true; // add a new pending action
                setTimeout(function(){
                    underlay.style.height = body.scrollHeight + "px";
                    scrollHeight = body.scrollHeight;
                    pending = false;
                }, 1000); // act rarely to stay fast
            }
        }

        addEvent("DOMNodeInserted", refresh);
        addEvent("DOMNodeRemoved",  refresh);
    }
    initdone = true;
}

/************************************************
 * SCROLLING
 ************************************************/

var que = [];
var pending = false;

/**
 * Pushes scroll actions to a given direction Array.
 */
function scrollArray(elem, dir, multiplyX, multiplyY, delay) {

    delay || (delay = 1000);
    directionCheck(dir);

    // que contains the scroll commands
    // [start, x, y]
    que.push([multiplyX, multiplyY, +new Date, 0]); // [x, y, start, lastpos]

    function step() {

        var now = +new Date;
        var addX = 0;
        var addY = 0;
        var finished;
        var x = 0, y = 1, start = 2, last = 3; // {enum}

        for (var i = 0; i < que.length; i++) {

            var item = que[i];
            var elapsed = now - item[start];
            finished = (elapsed >= animtime);

            // scroll is [0, 1]
            var scroll = finished ? 1 : elapsed / animtime;
            // easing
            if (pulseAlgorithm) {
                scroll = pulse(scroll);
            }

            // scale and quantize to int so our pixel difference works:
            var iscroll = Math.floor(stepsize * scroll + 0.99);
            scroll = iscroll - item[last];
            item[last] = iscroll;

            // add this to the actual scrolling
            addX += item[x] * scroll;
            addY += item[y] * scroll;

            // delete and step back
            if (finished) {
                que.splice(i, 1); i--;
                continue;
            }
        }

        addX = addX >> 0; // toInt
        addY = addY >> 0; // toInt

        // scroll left
        if (multiplyX && addX) {
            var lastLeft = elem.scrollLeft;
            elem.scrollLeft += addX;

            // scroll left failed (edge)
            if (elem.scrollLeft === lastLeft) {
                multiplyX = 0;
            }
        }

        // scroll top
        if (multiplyY && addY) {
            var lastTop = elem.scrollTop;
            elem.scrollTop += addY;

            // scroll top failed (edge)
            if (elem.scrollTop === lastTop) {
                multiplyY = 0;
            }
        }

        // clean up if there's nothing left to do
        if (!multiplyX && !multiplyY) {
            que = [];
        }

        if (que.length) {
            setTimeout(step, delay / framerate + 1);
        } else {
            pending = false;
        }
    }

    // start a new que of actions
    if (!pending) {
        setTimeout(step, 0);
        pending = true;
    }
}

/***********************************************
 * EVENTS
 ***********************************************/

/**
 * Mouse wheel handler.
 * NOTE: function logic should be decomposed.
 * @param {Object} event
 */
function wheel(event) {

    if (!initdone) {
        init();
    }

    var target = event.target;

    // use default if there's no overflowing
    // element or default action is prevented
    var elem = overflowingAncestor(target);
    if (!elem || noscrollframe || event.defaultPrevented ||
        isNodeName(activeElement, "embed") ||
       (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
        return true;
    }

    deltaX = event.wheelDeltaX || 0;
    deltaY = event.wheelDeltaY || 0;

    // normalize deltas
    // synaptics seems to send 1 sometimes,
    // and 120 other times (fix)
    if (Math.abs(deltaX) > 1.2) {
        deltaX /= 120;
    }
    if (Math.abs(deltaY) > 1.2) {
        deltaY /= 120;
    }

    var dir = (deltaY > 0) ? "up" : "down";
    scrollArray(elem, dir, -deltaX, -deltaY);
    event.preventDefault();
}

/**
 * Keydown event handler.
 * @param {Object} event
 */
function keydown(event) {

    var target   = event.target;
    var modifier = event.ctrlKey || event.altKey || event.metaKey;

    // do nothing if user is editing text
    // or using a modifier key (except shift)
    if ( /input|textarea|embed/i.test(target.nodeName) ||
         target.isContentEditable ||
         event.defaultPrevented   ||
         modifier ) {
      return true;
    }
    // spacebar should trigger button press
    if (isNodeName(target, "button") &&
        event.keyCode === key.spacebar) {
      return true;
    }

    var scale, dir, shift;
    var elem = overflowingAncestor(activeElement);
    var clientHeight = elem.clientHeight;

    if (elem == document.body) {
        clientHeight = window.innerHeight;
    }

    switch (event.keyCode) {
        case key.up:
            scale = -arrowscroll;
            dir = "up";
            break;
        case key.down:
            scale = arrowscroll;
            dir = "down";
            break;
        case key.spacebar: // (+ shift)
            shift = event.shiftKey ? 1 : -1;
            scale = -shift * clientHeight * 0.9;
            dir = (shift > 0) ? "up" : "down";
            break;
        case key.pageup:
            scale = -clientHeight * 0.9;
            dir = "up";
            break;
        case key.pagedown:
            scale = clientHeight * 0.9;
            dir = "down";
            break;
        case key.home:
            scale = -elem.scrollTop;
            dir = "up";
            break;
        case key.end:
            var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
            scale = (damt > 0) ? damt+10 : 0;
            dir = "down";
            break;
        default:
            return true; // a key we don"t care about
    }
    scale /= stepsize;
    scrollArray(elem, dir, 0, scale);
    event.preventDefault();
}

/**
 * Mousedown event only for updating activeElement
 */
function mousedown(event) {
    activeElement = event.target;
}

/***********************************************
 * HELPERS
 ***********************************************/

function addEvent(type, fn, bubble) {
    window.addEventListener(type, fn, (bubble||false));
}

function removeEvent(type, fn, bubble) {
    window.removeEventListener(type, fn, (bubble||false));
}

function isNodeName(el, tag) {
    return el.nodeName.toLowerCase() === tag.toLowerCase();
}

function directionCheck(dir) {
    if (direction !== dir) {
        que = [];
        direction = dir;
    }
}

function overflowingAncestor(el) {
    var bodyScrollHeight = document.body.scrollHeight;
    do {
        if (bodyScrollHeight === el.scrollHeight) {
            return document.body;
        }
        else if (el.clientHeight + 10 < el.scrollHeight) {
            overflow = getComputedStyle(el, "").getPropertyValue("overflow");
            if (overflow === "scroll" || overflow === "auto") {
                return el;
            }
        }
    } while (el = el.parentNode);
}

/***********************************************
 * PULSE
 ***********************************************/

/**
 * Viscous fluid with a pulse for part and decay for the rest.
 * - Applies a fixed force over an interval (a damped acceleration), and
 * - Lets the exponential bleed away the velocity over a longer interval
 * - Michael Herf, http://stereopsis.com/stopping/
 */
function pulse_(x) {
    var val, start, expx;
    // test
    x = x * pulseScale;
    if (x < 1) { // acceleartion
        val = x - (1 - Math.exp(-x));
    } else {     // tail
        // the previous animation ended here:
        start = Math.exp(-1);
        // simple viscous drag
        x -= 1;
        expx = 1 - Math.exp(-x);
        val = start + (expx * (1 - start));
    }
    return val * pulseNormalize;
}

function computePulseScale() {
    pulseNormalize = 1 / pulse_(1);
}

/**
 * Viscous fluid with a pulse for part and decay for the rest
 */
function pulse(x) {
    if (x >= 1) return 1;
    if (x <= 0) return 0;

    if (pulseNormalize == 1) {
        computePulseScale();
    }
    return pulse_(x);
}

addEvent("mousedown", mousedown);
addEvent("mousewheel", wheel);
addEvent("DOMContentLoaded", init);
<commit_msg>removed setupScrolls()<commit_after>
// SmoothScroll v0.9.4
// Licensed under the terms of the MIT license.

// People involved
// - Balazs Galambosi: maintainer (CHANGELOG.txt)
// - Patrick Brunner (patrickb1991@gmail.com)
// - Michael Herf: Pulse Algorithm

// Frame Variables
var frame         = false;
var noscrollframe = false;

// Scroll Variables (tweakable)
var framerate = 150; // [Hz]
var animtime  = 400; // [px]
var stepsize  = 120; // [px]

// Pulse (less tweakable)
// ratio of "tail" to "acceleration"
var pulseAlgorithm = true;
var pulseScale     = 8;
var pulseNormalize = 1;

// Keyboard Settings
var keyboardsupport = true;
var disableKeyboard = false;
var arrowscroll     = 50; // [px]

// Excluded pages
var exclude = "";

// Other Variables
var scrolls;
var deltaX = 0;
var deltaY = 0;
var direction = "down";
var initdone  = false;
var activeElement;

var key = { up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 };

/***********************************************
 * SETTINGS
 ***********************************************/

chrome.extension.connect({ name: "smoothscroll" }).
onMessage.addListener(function (settings) {

    // NOTE: + converts to {Number}
    framerate = Math.min(+settings.framerate, 250);
    animtime  = +settings.animtime;
    stepsize  = +settings.scrollsz;
    exclude   = settings.exclude;
    pulseAlgorithm  = (settings.pulseAlgorithm == "true");
    pulseScale      = +settings.pulseScale;
    keyboardsupport = (settings.keyboardsupport == "true");
    arrowscroll     = +settings.arrscroll;

    computePulseScale();

    // it seems that sometimes settings come late
    // and we need to test again for excluded pages
    initTest();

    if (keyboardsupport && !disableKeyboard) {
        addEvent("keydown", keydown);
    }

    // If extension settings were deleted somehow
    if (!framerate) {
        alert("SmoothScroll: Please restart Chrome");
    }
});

/***********************************************
 * INITIALIZE
 ***********************************************/

/**
 * Tests if smooth scrolling is allowed. Shuts down everything if not.
 */
function initTest() {

    // disable keys for google reader (spacebar conflict)
    if (document.URL.indexOf("google.com/reader/view") > -1) {
        disableKeyboard = true;
    }

    // disable everything if the page is blacklisted
    if (exclude) {
        var domains = exclude.split(/[,\n] ?/);
        for (var i = domains.length; i--;) {
            if (document.URL.indexOf(domains[i]) > -1) {
                removeEvent("mousewheel", wheel);
                disableKeyboard = true;
                break;
            }
        }
    }

    // disable keyboard support if anything above requested it
    if (disableKeyboard) {
        removeEvent("keydown", keydown);
    }
}

/**
 * Sets up scrolls array, determines if frames are involved.
 */
function init() {

    var body  = document.body;
    var docel = document.documentElement;
    var htmlHeight   = docel.offsetHeight;
    var windowHeight = window.innerHeight;

    activeElement = body;

    initTest();

    // Checks if this script is running in a frame
    if (top && top != self) {
        frame = true;
        if (docel.scrollHeight <= docel.clientHeight + 10) {
            noscrollframe = true;
        }
    }

    /**
     * This fixes a bug where the areas left and right to
     * the content does not trigger the onmousewheel event
     * on some pages. e.g.: html, body { height: 100% }
     */
    else if (body.scrollHeight > windowHeight &&
            (body.offsetHeight <= windowHeight || htmlHeight <= windowHeight)) {

        var underlay = document.createElement("div");
        underlay.setAttribute( "style",
            "z-index: -1; position: absolute; top: 0; left: 0; " +
            "width: 100%; height: " + body.scrollHeight + "px;" );
        body.appendChild(underlay);

        var scrollHeight = body.scrollHeight;
        var pending = false;

        // DOMChange delayed
        var refresh = function() {
            if (!pending && scrollHeight != body.scrollHeight) {
                pending = true; // add a new pending action
                setTimeout(function(){
                    underlay.style.height = body.scrollHeight + "px";
                    scrollHeight = body.scrollHeight;
                    pending = false;
                }, 1000); // act rarely to stay fast
            }
        }

        addEvent("DOMNodeInserted", refresh);
        addEvent("DOMNodeRemoved",  refresh);
    }
    initdone = true;
}

/************************************************
 * SCROLLING
 ************************************************/

var que = [];
var pending = false;

/**
 * Pushes scroll actions to a given direction Array.
 */
function scrollArray(elem, dir, multiplyX, multiplyY, delay) {

    delay || (delay = 1000);
    directionCheck(dir);

    // que contains the scroll commands [start, x, y]
    que.push([multiplyX, multiplyY, +new Date, 0]); // [x, y, start, lastpos]

    function step() {

        var now = +new Date;
        var addX = 0;
        var addY = 0;
        var finished;
        var x = 0, y = 1, start = 2, last = 3; // {enum}

        for (var i = 0; i < que.length; i++) {

            var item = que[i];
            var elapsed = now - item[start];
            finished = (elapsed >= animtime);

            // scroll is [0, 1]
            var scroll = finished ? 1 : elapsed / animtime;
            // easing
            if (pulseAlgorithm) {
                scroll = pulse(scroll);
            }

            // scale and quantize to int so our pixel difference works:
            var iscroll = Math.floor(stepsize * scroll + 0.99);
            scroll = iscroll - item[last];
            item[last] = iscroll;

            // add this to the actual scrolling
            addX += item[x] * scroll;
            addY += item[y] * scroll;

            // delete and step back
            if (finished) {
                que.splice(i, 1); i--;
                continue;
            }
        }

        addX = addX >> 0; // toInt
        addY = addY >> 0; // toInt

        // scroll left
        if (multiplyX && addX) {
            var lastLeft = elem.scrollLeft;
            elem.scrollLeft += addX;

            // scroll left failed (edge)
            if (elem.scrollLeft === lastLeft) {
                multiplyX = 0;
            }
        }

        // scroll top
        if (multiplyY && addY) {
            var lastTop = elem.scrollTop;
            elem.scrollTop += addY;

            // scroll top failed (edge)
            if (elem.scrollTop === lastTop) {
                multiplyY = 0;
            }
        }

        // clean up if there's nothing left to do
        if (!multiplyX && !multiplyY) {
            que = [];
        }

        if (que.length) {
            setTimeout(step, delay / framerate + 1);
        } else {
            pending = false;
        }
    }

    // start a new que of actions
    if (!pending) {
        setTimeout(step, 0);
        pending = true;
    }
}

/***********************************************
 * EVENTS
 ***********************************************/

/**
 * Mouse wheel handler.
 * NOTE: function logic should be decomposed.
 * @param {Object} event
 */
function wheel(event) {

    if (!initdone) {
        init();
    }

    var target = event.target;

    // use default if there's no overflowing
    // element or default action is prevented
    var elem = overflowingAncestor(target);
    if (!elem || noscrollframe || event.defaultPrevented ||
        isNodeName(activeElement, "embed") ||
       (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
        return true;
    }

    deltaX = event.wheelDeltaX || 0;
    deltaY = event.wheelDeltaY || 0;

    // normalize deltas
    // synaptics seems to send 1 sometimes,
    // and 120 other times (fix)
    if (Math.abs(deltaX) > 1.2) {
        deltaX /= 120;
    }
    if (Math.abs(deltaY) > 1.2) {
        deltaY /= 120;
    }

    var dir = (deltaY > 0) ? "up" : "down";
    scrollArray(elem, dir, -deltaX, -deltaY);
    event.preventDefault();
}

/**
 * Keydown event handler.
 * @param {Object} event
 */
function keydown(event) {

    var target   = event.target;
    var modifier = event.ctrlKey || event.altKey || event.metaKey;

    // do nothing if user is editing text
    // or using a modifier key (except shift)
    if ( /input|textarea|embed/i.test(target.nodeName) ||
         target.isContentEditable ||
         event.defaultPrevented   ||
         modifier ) {
      return true;
    }
    // spacebar should trigger button press
    if (isNodeName(target, "button") &&
        event.keyCode === key.spacebar) {
      return true;
    }

    var scale, dir, shift;
    var elem = overflowingAncestor(activeElement);
    var clientHeight = elem.clientHeight;

    if (elem == document.body) {
        clientHeight = window.innerHeight;
    }

    switch (event.keyCode) {
        case key.up:
            scale = -arrowscroll;
            dir = "up";
            break;
        case key.down:
            scale = arrowscroll;
            dir = "down";
            break;
        case key.spacebar: // (+ shift)
            shift = event.shiftKey ? 1 : -1;
            scale = -shift * clientHeight * 0.9;
            dir = (shift > 0) ? "up" : "down";
            break;
        case key.pageup:
            scale = -clientHeight * 0.9;
            dir = "up";
            break;
        case key.pagedown:
            scale = clientHeight * 0.9;
            dir = "down";
            break;
        case key.home:
            scale = -elem.scrollTop;
            dir = "up";
            break;
        case key.end:
            var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
            scale = (damt > 0) ? damt+10 : 0;
            dir = "down";
            break;
        default:
            return true; // a key we don"t care about
    }
    scale /= stepsize;
    scrollArray(elem, dir, 0, scale);
    event.preventDefault();
}

/**
 * Mousedown event only for updating activeElement
 */
function mousedown(event) {
    activeElement = event.target;
}

/***********************************************
 * HELPERS
 ***********************************************/

function addEvent(type, fn, bubble) {
    window.addEventListener(type, fn, (bubble||false));
}

function removeEvent(type, fn, bubble) {
    window.removeEventListener(type, fn, (bubble||false));
}

function isNodeName(el, tag) {
    return el.nodeName.toLowerCase() === tag.toLowerCase();
}

function directionCheck(dir) {
    if (direction !== dir) {
        que = [];
        direction = dir;
    }
}

function overflowingAncestor(el) {
    var bodyScrollHeight = document.body.scrollHeight;
    do {
        if (bodyScrollHeight === el.scrollHeight) {
            return document.body;
        }
        else if (el.clientHeight + 10 < el.scrollHeight) {
            overflow = getComputedStyle(el, "").getPropertyValue("overflow");
            if (overflow === "scroll" || overflow === "auto") {
                return el;
            }
        }
    } while (el = el.parentNode);
}

/***********************************************
 * PULSE
 ***********************************************/

/**
 * Viscous fluid with a pulse for part and decay for the rest.
 * - Applies a fixed force over an interval (a damped acceleration), and
 * - Lets the exponential bleed away the velocity over a longer interval
 * - Michael Herf, http://stereopsis.com/stopping/
 */
function pulse_(x) {
    var val, start, expx;
    // test
    x = x * pulseScale;
    if (x < 1) { // acceleartion
        val = x - (1 - Math.exp(-x));
    } else {     // tail
        // the previous animation ended here:
        start = Math.exp(-1);
        // simple viscous drag
        x -= 1;
        expx = 1 - Math.exp(-x);
        val = start + (expx * (1 - start));
    }
    return val * pulseNormalize;
}

function computePulseScale() {
    pulseNormalize = 1 / pulse_(1);
}

/**
 * Viscous fluid with a pulse for part and decay for the rest
 */
function pulse(x) {
    if (x >= 1) return 1;
    if (x <= 0) return 0;

    if (pulseNormalize == 1) {
        computePulseScale();
    }
    return pulse_(x);
}

addEvent("mousedown", mousedown);
addEvent("mousewheel", wheel);
addEvent("DOMContentLoaded", init);
<|endoftext|>


<commit_before>define("dijit/_TemplatedMixin", ["dojo", "dijit", "dijit/_Widget", "dojo/string", "dojo/cache"], function(dojo, dijit) {

dojo.declare("dijit._TemplatedMixin",
	null,
	{
		// summary:
		//		Mixin for widgets that are instantiated from a template

		// templateString: [protected] String
		//		A string that represents the widget template.
		//		Use in conjunction with dojo.cache() to load from a file.
		templateString: null,

		// templatePath: [protected deprecated] String
		//		Path to template (HTML file) for this widget relative to dojo.baseUrl.
		//		Deprecated: use templateString with dojo.cache() instead.
		templatePath: null,

		// skipNodeCache: [protected] Boolean
		//		If using a cached widget template nodes poses issues for a
		//		particular widget class, it can set this property to ensure
		//		that its template is always re-built from a string
		_skipNodeCache: false,

		// _earlyTemplatedStartup: Boolean
		//		A fallback to preserve the 1.0 - 1.3 behavior of children in
		//		templates having their startup called before the parent widget
		//		fires postCreate. Defaults to 'false', causing child widgets to
		//		have their .startup() called immediately before a parent widget
		//		.startup(), but always after the parent .postCreate(). Set to
		//		'true' to re-enable to previous, arguably broken, behavior.
		_earlyTemplatedStartup: false,

/*=====
		// _attachPoints: [private] String[]
		//		List of widget attribute names associated with dojoAttachPoint=... in the
		//		template, ex: ["containerNode", "labelNode"]
 		_attachPoints: [],
 =====*/

/*=====
		// _attachEvents: [private] Handle[]
		//		List of connections associated with dojoAttachEvent=... in the
		//		template
 		_attachEvents: [],
 =====*/

		constructor: function(){
			this._attachPoints = [];
			this._attachEvents = [];
		},

		_stringRepl: function(tmpl){
			// summary:
			//		Does substitution of ${foo} type properties in template string
			// tags:
			//		private
			var className = this.declaredClass, _this = this;
			// Cache contains a string because we need to do property replacement
			// do the property replacement
			return dojo.string.substitute(tmpl, this, function(value, key){
				if(key.charAt(0) == '!'){ value = dojo.getObject(key.substr(1), false, _this); }
				if(typeof value == "undefined"){ throw new Error(className+" template:"+key); } // a debugging aide
				if(value == null){ return ""; }

				// Substitution keys beginning with ! will skip the transform step,
				// in case a user wishes to insert unescaped markup, e.g. ${!foo}
				return key.charAt(0) == "!" ? value :
					// Safer substitution, see heading "Attribute values" in
					// http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
					value.toString().replace(/"/g,"""); //TODO: add &amp? use encodeXML method?
			}, this);
		},

		buildRendering: function(){
			// summary:
			//		Construct the UI for this widget from a template, setting this.domNode.
			// tags:
			//		protected

			if(!this.templateString){
				this.templateString = dojo.cache(this.templatePath, {sanitize: true});
			}

			// Lookup cached version of template, and download to cache if it
			// isn't there already.  Returns either a DomNode or a string, depending on
			// whether or not the template contains ${foo} replacement parameters.
			var cached = dijit._TemplatedMixin.getCachedTemplate(this.templateString, this._skipNodeCache);

			var node;
			if(dojo.isString(cached)){
				node = dojo._toDom(this._stringRepl(cached));
				if(node.nodeType != 1){
					// Flag common problems such as templates with multiple top level nodes (nodeType == 11)
					throw new Error("Invalid template: " + cached);
				}
			}else{
				// if it's a node, all we have to do is clone it
				node = cached.cloneNode(true);
			}

			this.domNode = node;

			// Call down to _Widget.buildRendering() to get base classes assigned
			// TODO: change the baseClass assignment to _setBaseClassAttr
			this.inherited(arguments);

			// recurse through the node, looking for, and attaching to, our
			// attachment points and events, which should be defined on the template node.
			this._attachTemplateNodes(node, function(n,p){ return n.getAttribute(p); });

			this._beforeFillContent();		// hook for _WidgetsInTemplateMixin

			this._fillContent(this.srcNodeRef);
		},

		_beforeFillContent: function(){
		},

		_fillContent: function(/*DomNode*/ source){
			// summary:
			//		Relocate source contents to templated container node.
			//		this.containerNode must be able to receive children, or exceptions will be thrown.
			// tags:
			//		protected
			var dest = this.containerNode;
			if(source && dest){
				while(source.hasChildNodes()){
					dest.appendChild(source.firstChild);
				}
			}
		},

		_attachTemplateNodes: function(rootNode, getAttrFunc){
			// summary:
			//		Iterate through the template and attach functions and nodes accordingly.
			//		Alternately, if rootNode is an array of widgets, then will process dojoAttachPoint
			//		etc. for those widgets.
			// description:
			//		Map widget properties and functions to the handlers specified in
			//		the dom node and it's descendants. This function iterates over all
			//		nodes and looks for these properties:
			//			* dojoAttachPoint/data-dojo-attach-point
			//			* dojoAttachEvent/data-dojo-attach-event
			// rootNode: DomNode|Widget[]
			//		the node to search for properties. All children will be searched.
			// getAttrFunc: Function
			//		a function which will be used to obtain property for a given
			//		DomNode/Widget
			// tags:
			//		private

			var nodes = dojo.isArray(rootNode) ? rootNode : (rootNode.all || rootNode.getElementsByTagName("*"));
			var x = dojo.isArray(rootNode) ? 0 : -1;
			for(; x<nodes.length; x++){
				var baseNode = (x == -1) ? rootNode : nodes[x];
				if(this.widgetsInTemplate && (getAttrFunc(baseNode, "dojoType") || getAttrFunc(baseNode, "data-dojo-type"))){
					continue;
				}
				// Process dojoAttachPoint
				var attachPoint = getAttrFunc(baseNode, "dojoAttachPoint") || getAttrFunc(baseNode, "data-dojo-attach-point");
				if(attachPoint){
					var point, points = attachPoint.split(/\s*,\s*/);
					while((point = points.shift())){
						if(dojo.isArray(this[point])){
							this[point].push(baseNode);
						}else{
							this[point]=baseNode;
						}
						this._attachPoints.push(point);
					}
				}

				// Process dojoAttachEvent
				var attachEvent = getAttrFunc(baseNode, "dojoAttachEvent") || getAttrFunc(baseNode, "data-dojo-attach-event");;
				if(attachEvent){
					// NOTE: we want to support attributes that have the form
					// "domEvent: nativeEvent; ..."
					var event, events = attachEvent.split(/\s*,\s*/);
					var trim = dojo.trim;
					while((event = events.shift())){
						if(event){
							var thisFunc = null;
							if(event.indexOf(":") != -1){
								// oh, if only JS had tuple assignment
								var funcNameArr = event.split(":");
								event = trim(funcNameArr[0]);
								thisFunc = trim(funcNameArr[1]);
							}else{
								event = trim(event);
							}
							if(!thisFunc){
								thisFunc = event;
							}
							this._attachEvents.push(this.connect(baseNode, event, thisFunc));
						}
					}
				}
			}
		},

		destroyRendering: function(){
			// Delete all attach points to prevent IE6 memory leaks.
			dojo.forEach(this._attachPoints, function(point){
				delete this[point];
			}, this);
			this._attachPoints = [];

			// And same for event handlers
			dojo.forEach(this._attachEvents, this.disconnect, this);
			this._attachEvents = [];

			this.inherited(arguments);
		}
	}
);

// key is either templatePath or templateString; object is either string or DOM tree
dijit._TemplatedMixin._templateCache = {};

dijit._TemplatedMixin.getCachedTemplate = function(templateString, alwaysUseString){
	// summary:
	//		Static method to get a template based on the templatePath or
	//		templateString key
	// templateString: String
	//		The template
	// alwaysUseString: Boolean
	//		Don't cache the DOM tree for this template, even if it doesn't have any variables
	// returns: Mixed
	//		Either string (if there are ${} variables that need to be replaced) or just
	//		a DOM tree (if the node can be cloned directly)

	// is it already cached?
	var tmplts = dijit._TemplatedMixin._templateCache;
	var key = templateString;
	var cached = tmplts[key];
	if(cached){
		try{
			// if the cached value is an innerHTML string (no ownerDocument) or a DOM tree created within the current document, then use the current cached value
			if(!cached.ownerDocument || cached.ownerDocument == dojo.doc){
				// string or node of the same document
				return cached;
			}
		}catch(e){ /* squelch */ } // IE can throw an exception if cached.ownerDocument was reloaded
		dojo.destroy(cached);
	}

	templateString = dojo.string.trim(templateString);

	if(alwaysUseString || templateString.match(/\$\{([^\}]+)\}/g)){
		// there are variables in the template so all we can do is cache the string
		return (tmplts[key] = templateString); //String
	}else{
		// there are no variables in the template so we can cache the DOM tree
		var node = dojo._toDom(templateString);
		if(node.nodeType != 1){
			throw new Error("Invalid template: " + templateString);
		}
		return (tmplts[key] = node); //Node
	}
};

if(dojo.isIE){
	dojo.addOnWindowUnload(function(){
		var cache = dijit._TemplatedMixin._templateCache;
		for(var key in cache){
			var value = cache[key];
			if(typeof value == "object"){ // value is either a string or a DOM node template
				dojo.destroy(value);
			}
			delete cache[key];
		}
	});
}

// These arguments can be specified for widgets which are used in templates.
// Since any widget can be specified as sub widgets in template, mix it
// into the base widget class.  (This is a hack, but it's effective.)
dojo.extend(dijit._Widget,{
	dojoAttachEvent: "",
	dojoAttachPoint: ""
});

return dijit._TemplatedMixin;
});
<commit_msg>refs #12275: updated doc in _TemplatedMixin<commit_after>define("dijit/_TemplatedMixin", ["dojo", "dijit", "dijit/_Widget", "dojo/string", "dojo/cache"], function(dojo, dijit) {

dojo.declare("dijit._TemplatedMixin",
	null,
	{
		// summary:
		//		Mixin for widgets that are instantiated from a template

		// templateString: [protected] String
		//		A string that represents the widget template.
		//		Use in conjunction with dojo.cache() to load from a file.
		templateString: null,

		// templatePath: [protected deprecated] String
		//		Path to template (HTML file) for this widget relative to dojo.baseUrl.
		//		Deprecated: use templateString with dojo.cache() instead.
		templatePath: null,

		// skipNodeCache: [protected] Boolean
		//		If using a cached widget template nodes poses issues for a
		//		particular widget class, it can set this property to ensure
		//		that its template is always re-built from a string
		_skipNodeCache: false,

		// _earlyTemplatedStartup: Boolean
		//		A fallback to preserve the 1.0 - 1.3 behavior of children in
		//		templates having their startup called before the parent widget
		//		fires postCreate. Defaults to 'false', causing child widgets to
		//		have their .startup() called immediately before a parent widget
		//		.startup(), but always after the parent .postCreate(). Set to
		//		'true' to re-enable to previous, arguably broken, behavior.
		_earlyTemplatedStartup: false,

/*=====
		// _attachPoints: [private] String[]
		//		List of widget attribute names associated with dojoAttachPoint=... in the
		//		template, ex: ["containerNode", "labelNode"]
 		_attachPoints: [],
 =====*/

/*=====
		// _attachEvents: [private] Handle[]
		//		List of connections associated with dojoAttachEvent=... in the
		//		template
 		_attachEvents: [],
 =====*/

		constructor: function(){
			this._attachPoints = [];
			this._attachEvents = [];
		},

		_stringRepl: function(tmpl){
			// summary:
			//		Does substitution of ${foo} type properties in template string
			// tags:
			//		private
			var className = this.declaredClass, _this = this;
			// Cache contains a string because we need to do property replacement
			// do the property replacement
			return dojo.string.substitute(tmpl, this, function(value, key){
				if(key.charAt(0) == '!'){ value = dojo.getObject(key.substr(1), false, _this); }
				if(typeof value == "undefined"){ throw new Error(className+" template:"+key); } // a debugging aide
				if(value == null){ return ""; }

				// Substitution keys beginning with ! will skip the transform step,
				// in case a user wishes to insert unescaped markup, e.g. ${!foo}
				return key.charAt(0) == "!" ? value :
					// Safer substitution, see heading "Attribute values" in
					// http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
					value.toString().replace(/"/g,"""); //TODO: add &amp? use encodeXML method?
			}, this);
		},

		buildRendering: function(){
			// summary:
			//		Construct the UI for this widget from a template, setting this.domNode.
			// tags:
			//		protected

			if(!this.templateString){
				this.templateString = dojo.cache(this.templatePath, {sanitize: true});
			}

			// Lookup cached version of template, and download to cache if it
			// isn't there already.  Returns either a DomNode or a string, depending on
			// whether or not the template contains ${foo} replacement parameters.
			var cached = dijit._TemplatedMixin.getCachedTemplate(this.templateString, this._skipNodeCache);

			var node;
			if(dojo.isString(cached)){
				node = dojo._toDom(this._stringRepl(cached));
				if(node.nodeType != 1){
					// Flag common problems such as templates with multiple top level nodes (nodeType == 11)
					throw new Error("Invalid template: " + cached);
				}
			}else{
				// if it's a node, all we have to do is clone it
				node = cached.cloneNode(true);
			}

			this.domNode = node;

			// Call down to _Widget.buildRendering() to get base classes assigned
			// TODO: change the baseClass assignment to _setBaseClassAttr
			this.inherited(arguments);

			// recurse through the node, looking for, and attaching to, our
			// attachment points and events, which should be defined on the template node.
			this._attachTemplateNodes(node, function(n,p){ return n.getAttribute(p); });

			this._beforeFillContent();		// hook for _WidgetsInTemplateMixin

			this._fillContent(this.srcNodeRef);
		},

		_beforeFillContent: function(){
		},

		_fillContent: function(/*DomNode*/ source){
			// summary:
			//		Relocate source contents to templated container node.
			//		this.containerNode must be able to receive children, or exceptions will be thrown.
			// tags:
			//		protected
			var dest = this.containerNode;
			if(source && dest){
				while(source.hasChildNodes()){
					dest.appendChild(source.firstChild);
				}
			}
		},

		_attachTemplateNodes: function(rootNode, getAttrFunc){
			// summary:
			//		Iterate through the template and attach functions and nodes accordingly.
			//		Alternately, if rootNode is an array of widgets, then will process dojoAttachPoint
			//		etc. for those widgets.
			// description:
			//		Map widget properties and functions to the handlers specified in
			//		the dom node and it's descendants. This function iterates over all
			//		nodes and looks for these properties:
			//			* dojoAttachPoint/data-dojo-attach-point
			//			* dojoAttachEvent/data-dojo-attach-event
			// rootNode: DomNode|Widget[]
			//		the node to search for properties. All children will be searched.
			// getAttrFunc: Function
			//		a function which will be used to obtain property for a given
			//		DomNode/Widget
			// tags:
			//		private

			var nodes = dojo.isArray(rootNode) ? rootNode : (rootNode.all || rootNode.getElementsByTagName("*"));
			var x = dojo.isArray(rootNode) ? 0 : -1;
			for(; x<nodes.length; x++){
				var baseNode = (x == -1) ? rootNode : nodes[x];
				if(this.widgetsInTemplate && (getAttrFunc(baseNode, "dojoType") || getAttrFunc(baseNode, "data-dojo-type"))){
					continue;
				}
				// Process dojoAttachPoint
				var attachPoint = getAttrFunc(baseNode, "dojoAttachPoint") || getAttrFunc(baseNode, "data-dojo-attach-point");
				if(attachPoint){
					var point, points = attachPoint.split(/\s*,\s*/);
					while((point = points.shift())){
						if(dojo.isArray(this[point])){
							this[point].push(baseNode);
						}else{
							this[point]=baseNode;
						}
						this._attachPoints.push(point);
					}
				}

				// Process dojoAttachEvent
				var attachEvent = getAttrFunc(baseNode, "dojoAttachEvent") || getAttrFunc(baseNode, "data-dojo-attach-event");
				if(attachEvent){
					// NOTE: we want to support attributes that have the form
					// "domEvent: nativeEvent; ..."
					var event, events = attachEvent.split(/\s*,\s*/);
					var trim = dojo.trim;
					while((event = events.shift())){
						if(event){
							var thisFunc = null;
							if(event.indexOf(":") != -1){
								// oh, if only JS had tuple assignment
								var funcNameArr = event.split(":");
								event = trim(funcNameArr[0]);
								thisFunc = trim(funcNameArr[1]);
							}else{
								event = trim(event);
							}
							if(!thisFunc){
								thisFunc = event;
							}
							this._attachEvents.push(this.connect(baseNode, event, thisFunc));
						}
					}
				}
			}
		},

		destroyRendering: function(){
			// Delete all attach points to prevent IE6 memory leaks.
			dojo.forEach(this._attachPoints, function(point){
				delete this[point];
			}, this);
			this._attachPoints = [];

			// And same for event handlers
			dojo.forEach(this._attachEvents, this.disconnect, this);
			this._attachEvents = [];

			this.inherited(arguments);
		}
	}
);

// key is templateString; object is either string or DOM tree
dijit._TemplatedMixin._templateCache = {};

dijit._TemplatedMixin.getCachedTemplate = function(templateString, alwaysUseString){
	// summary:
	//		Static method to get a template based on the templatePath or
	//		templateString key
	// templateString: String
	//		The template
	// alwaysUseString: Boolean
	//		Don't cache the DOM tree for this template, even if it doesn't have any variables
	// returns: Mixed
	//		Either string (if there are ${} variables that need to be replaced) or just
	//		a DOM tree (if the node can be cloned directly)

	// is it already cached?
	var tmplts = dijit._TemplatedMixin._templateCache;
	var key = templateString;
	var cached = tmplts[key];
	if(cached){
		try{
			// if the cached value is an innerHTML string (no ownerDocument) or a DOM tree created within the current document, then use the current cached value
			if(!cached.ownerDocument || cached.ownerDocument == dojo.doc){
				// string or node of the same document
				return cached;
			}
		}catch(e){ /* squelch */ } // IE can throw an exception if cached.ownerDocument was reloaded
		dojo.destroy(cached);
	}

	templateString = dojo.string.trim(templateString);

	if(alwaysUseString || templateString.match(/\$\{([^\}]+)\}/g)){
		// there are variables in the template so all we can do is cache the string
		return (tmplts[key] = templateString); //String
	}else{
		// there are no variables in the template so we can cache the DOM tree
		var node = dojo._toDom(templateString);
		if(node.nodeType != 1){
			throw new Error("Invalid template: " + templateString);
		}
		return (tmplts[key] = node); //Node
	}
};

if(dojo.isIE){
	dojo.addOnWindowUnload(function(){
		var cache = dijit._TemplatedMixin._templateCache;
		for(var key in cache){
			var value = cache[key];
			if(typeof value == "object"){ // value is either a string or a DOM node template
				dojo.destroy(value);
			}
			delete cache[key];
		}
	});
}

// These arguments can be specified for widgets which are used in templates.
// Since any widget can be specified as sub widgets in template, mix it
// into the base widget class.  (This is a hack, but it's effective.)
dojo.extend(dijit._Widget,{
	dojoAttachEvent: "",
	dojoAttachPoint: ""
});

return dijit._TemplatedMixin;
});
<|endoftext|>


<commit_before>const request = require('supertest')
const assert = require('assert')
const crypto = require('crypto')
const path = require('path')
const zlib = require('zlib')
const Koa = require('koa')
const fs = require('fs')

const compress = require('..')

describe('Compress', () => {
  const buffer = crypto.randomBytes(1024)
  const string = buffer.toString('hex')

  function sendString (ctx, next) {
    ctx.body = string
  }

  function sendBuffer (ctx, next) {
    ctx.compress = true
    ctx.body = buffer
  }

  let server
  afterEach(() => { if (server) server.close() })

  test('should compress strings', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
    assert.equal(res.text, string)
  })

  test('should not compress strings below threshold', async () => {
    const app = new Koa()

    app.use(compress({
      threshold: '1mb'
    }))
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['content-length'], '2048')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.text, string)
  })

  test('should compress JSON body', async () => {
    const app = new Koa()
    const jsonBody = { status: 200, message: 'ok', data: string }

    app.use(compress())
    app.use((ctx, next) => {
      ctx.body = jsonBody
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
    assert.equal(res.text, JSON.stringify(jsonBody))
  })

  test('should not compress JSON body below threshold', async () => {
    const app = new Koa()
    const jsonBody = { status: 200, message: 'ok' }

    app.use(compress())
    app.use((ctx, next) => {
      ctx.body = jsonBody
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.text, JSON.stringify(jsonBody))
  })

  test('should compress buffers', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should compress streams', async () => {
    const app = new Koa()

    app.use(compress())

    app.use((ctx, next) => {
      ctx.type = 'application/javascript'
      ctx.body = fs.createReadStream(path.join(__dirname, 'index.js'))
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    // res.should.have.header('Content-Encoding', 'gzip')
    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should compress when ctx.compress === true', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should not compress when ctx.compress === false', async () => {
    const app = new Koa()

    app.use(compress())
    app.use((ctx, next) => {
      ctx.compress = false
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['content-length'], '1024')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
  })

  test('should not compress HEAD requests', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .head('/')

    assert(!res.headers['content-encoding'])
  })

  test('should not crash even if accept-encoding: sdch', () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    return request(server)
      .get('/')
      .set('Accept-Encoding', 'sdch, gzip, deflate')
      .expect(200)
  })

  test('should not compress if no accept-encoding is sent (with the default)', async () => {
    const app = new Koa()
    app.use(compress({
      threshold: 0
    }))
    app.use((ctx) => {
      ctx.type = 'text'
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', '')

    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.headers['content-length'], '1024')
    assert.equal(res.headers.vary, 'Accept-Encoding')
  })

  test('should be gzip if no accept-encoding is sent (with the standard default)', async () => {
    const app = new Koa()
    app.use(compress({
      threshold: 0,
      defaultEncoding: '*'
    }))
    app.use((ctx) => {
      ctx.type = 'text'
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', '')

    assert.equal(res.headers['content-encoding'], 'gzip')
    assert.equal(res.headers.vary, 'Accept-Encoding')
  })

  test('should not crash if a type does not pass the filter', () => {
    const app = new Koa()

    app.use(compress())
    app.use((ctx) => {
      ctx.type = 'image/png'
      ctx.body = Buffer.alloc(2048)
    })
    server = app.listen()

    return request(server)
      .get('/')
      .expect(200)
  })

  test('should not compress when transfer-encoding is already set', () => {
    const app = new Koa()

    app.use(compress({
      threshold: 0
    }))
    app.use((ctx) => {
      ctx.set('Content-Encoding', 'identity')
      ctx.type = 'text'
      ctx.body = 'asdf'
    })
    server = app.listen()

    return request(server)
      .get('/')
      .expect('asdf')
  })

  test('should support Z_SYNC_FLUSH', async () => {
    const app = new Koa()

    app.use(compress({
      flush: zlib.constants.Z_SYNC_FLUSH
    }))
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
    assert.equal(res.text, string)
  })

  describe('Cache-Control', () => {
    ['no-transform', 'public, no-transform', 'no-transform, private', 'no-transform , max-age=1000', 'max-age=1000 , no-transform'].forEach(headerValue => {
      test(`should skip Cache-Control: ${headerValue}`, async () => {
        const app = new Koa()

        app.use(compress())
        app.use((ctx, next) => {
          ctx.set('Cache-Control', headerValue)
          next()
        })
        app.use(sendString)
        server = app.listen()

        const res = await request(server)
          .get('/')
          .expect(200)

        assert.equal(res.headers['content-length'], '2048')
        assert.equal(res.headers.vary, 'Accept-Encoding')
        assert(!res.headers['content-encoding'])
        assert(!res.headers['transfer-encoding'])
        assert.equal(res.text, string)
      })
    });

    ['not-no-transform', 'public', 'no-transform-thingy'].forEach(headerValue => {
      test(`should not skip Cache-Control: ${headerValue}`, async () => {
        const app = new Koa()

        app.use(compress())
        app.use((ctx, next) => {
          ctx.set('Cache-Control', headerValue)
          next()
        })
        app.use(sendString)
        server = app.listen()

        const res = await request(server)
          .get('/')
          .expect(200)

        assert.equal(res.headers['transfer-encoding'], 'chunked')
        assert.equal(res.headers.vary, 'Accept-Encoding')
        assert(!res.headers['content-length'])
        assert.equal(res.text, string)
      })
    })
  })

  test('accept-encoding: deflate', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'deflate')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'deflate')
  })

  test('accept-encoding: gzip', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'gzip, deflate')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'gzip')
  })

  if (process.versions.brotli) {
    test('accept-encoding: br', async () => {
      const app = new Koa()

      app.use(compress())
      app.use(sendBuffer)
      server = app.listen()

      const res = await request(server)
        .get('/')
        .set('Accept-Encoding', 'br')
        .expect(200)

      assert.equal(res.headers.vary, 'Accept-Encoding')
      assert.equal(res.headers['content-encoding'], 'br')
    })
  }

  test('accept-encoding: br (banned, should be gzip)', async () => {
    const app = new Koa()

    app.use(compress({ br: false }))
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'gzip, deflate, br')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'gzip')
  })
})
<commit_msg>test: remove invalid test. closes #123<commit_after>const request = require('supertest')
const assert = require('assert')
const crypto = require('crypto')
const path = require('path')
const zlib = require('zlib')
const Koa = require('koa')
const fs = require('fs')

const compress = require('..')

describe('Compress', () => {
  const buffer = crypto.randomBytes(1024)
  const string = buffer.toString('hex')

  function sendString (ctx, next) {
    ctx.body = string
  }

  function sendBuffer (ctx, next) {
    ctx.compress = true
    ctx.body = buffer
  }

  let server
  afterEach(() => { if (server) server.close() })

  test('should compress strings', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
    assert.equal(res.text, string)
  })

  test('should not compress strings below threshold', async () => {
    const app = new Koa()

    app.use(compress({
      threshold: '1mb'
    }))
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['content-length'], '2048')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.text, string)
  })

  test('should compress JSON body', async () => {
    const app = new Koa()
    const jsonBody = { status: 200, message: 'ok', data: string }

    app.use(compress())
    app.use((ctx, next) => {
      ctx.body = jsonBody
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
    assert.equal(res.text, JSON.stringify(jsonBody))
  })

  test('should not compress JSON body below threshold', async () => {
    const app = new Koa()
    const jsonBody = { status: 200, message: 'ok' }

    app.use(compress())
    app.use((ctx, next) => {
      ctx.body = jsonBody
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.text, JSON.stringify(jsonBody))
  })

  test('should compress buffers', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should compress streams', async () => {
    const app = new Koa()

    app.use(compress())

    app.use((ctx, next) => {
      ctx.type = 'application/javascript'
      ctx.body = fs.createReadStream(path.join(__dirname, 'index.js'))
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    // res.should.have.header('Content-Encoding', 'gzip')
    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should compress when ctx.compress === true', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['transfer-encoding'], 'chunked')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-length'])
  })

  test('should not compress when ctx.compress === false', async () => {
    const app = new Koa()

    app.use(compress())
    app.use((ctx, next) => {
      ctx.compress = false
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .expect(200)

    assert.equal(res.headers['content-length'], '1024')
    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
  })

  test('should not compress HEAD requests', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendString)
    server = app.listen()

    const res = await request(server)
      .head('/')

    assert(!res.headers['content-encoding'])
  })

  test('should not crash even if accept-encoding: sdch', () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    return request(server)
      .get('/')
      .set('Accept-Encoding', 'sdch, gzip, deflate')
      .expect(200)
  })

  test('should not compress if no accept-encoding is sent (with the default)', async () => {
    const app = new Koa()
    app.use(compress({
      threshold: 0
    }))
    app.use((ctx) => {
      ctx.type = 'text'
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', '')

    assert(!res.headers['content-encoding'])
    assert(!res.headers['transfer-encoding'])
    assert.equal(res.headers['content-length'], '1024')
    assert.equal(res.headers.vary, 'Accept-Encoding')
  })

  test('should be gzip if no accept-encoding is sent (with the standard default)', async () => {
    const app = new Koa()
    app.use(compress({
      threshold: 0,
      defaultEncoding: '*'
    }))
    app.use((ctx) => {
      ctx.type = 'text'
      ctx.body = buffer
    })
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', '')

    assert.equal(res.headers['content-encoding'], 'gzip')
    assert.equal(res.headers.vary, 'Accept-Encoding')
  })

  test('should not crash if a type does not pass the filter', () => {
    const app = new Koa()

    app.use(compress())
    app.use((ctx) => {
      ctx.type = 'image/png'
      ctx.body = Buffer.alloc(2048)
    })
    server = app.listen()

    return request(server)
      .get('/')
      .expect(200)
  })

  test('should not compress when transfer-encoding is already set', () => {
    const app = new Koa()

    app.use(compress({
      threshold: 0
    }))
    app.use((ctx) => {
      ctx.set('Content-Encoding', 'identity')
      ctx.type = 'text'
      ctx.body = 'asdf'
    })
    server = app.listen()

    return request(server)
      .get('/')
      .expect('asdf')
  })

  describe('Cache-Control', () => {
    ['no-transform', 'public, no-transform', 'no-transform, private', 'no-transform , max-age=1000', 'max-age=1000 , no-transform'].forEach(headerValue => {
      test(`should skip Cache-Control: ${headerValue}`, async () => {
        const app = new Koa()

        app.use(compress())
        app.use((ctx, next) => {
          ctx.set('Cache-Control', headerValue)
          next()
        })
        app.use(sendString)
        server = app.listen()

        const res = await request(server)
          .get('/')
          .expect(200)

        assert.equal(res.headers['content-length'], '2048')
        assert.equal(res.headers.vary, 'Accept-Encoding')
        assert(!res.headers['content-encoding'])
        assert(!res.headers['transfer-encoding'])
        assert.equal(res.text, string)
      })
    });

    ['not-no-transform', 'public', 'no-transform-thingy'].forEach(headerValue => {
      test(`should not skip Cache-Control: ${headerValue}`, async () => {
        const app = new Koa()

        app.use(compress())
        app.use((ctx, next) => {
          ctx.set('Cache-Control', headerValue)
          next()
        })
        app.use(sendString)
        server = app.listen()

        const res = await request(server)
          .get('/')
          .expect(200)

        assert.equal(res.headers['transfer-encoding'], 'chunked')
        assert.equal(res.headers.vary, 'Accept-Encoding')
        assert(!res.headers['content-length'])
        assert.equal(res.text, string)
      })
    })
  })

  test('accept-encoding: deflate', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'deflate')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'deflate')
  })

  test('accept-encoding: gzip', async () => {
    const app = new Koa()

    app.use(compress())
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'gzip, deflate')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'gzip')
  })

  if (process.versions.brotli) {
    test('accept-encoding: br', async () => {
      const app = new Koa()

      app.use(compress())
      app.use(sendBuffer)
      server = app.listen()

      const res = await request(server)
        .get('/')
        .set('Accept-Encoding', 'br')
        .expect(200)

      assert.equal(res.headers.vary, 'Accept-Encoding')
      assert.equal(res.headers['content-encoding'], 'br')
    })
  }

  test('accept-encoding: br (banned, should be gzip)', async () => {
    const app = new Koa()

    app.use(compress({ br: false }))
    app.use(sendBuffer)
    server = app.listen()

    const res = await request(server)
      .get('/')
      .set('Accept-Encoding', 'gzip, deflate, br')
      .expect(200)

    assert.equal(res.headers.vary, 'Accept-Encoding')
    assert.equal(res.headers['content-encoding'], 'gzip')
  })
})
<|endoftext|>


<commit_before>import BufferedContent from 'discourse/mixins/buffered-content';
import ScrollTop from 'discourse/mixins/scroll-top';
import SiteSetting from 'admin/models/site-setting';
import { propertyNotEqual } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';

const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list'];

export default Ember.Component.extend(BufferedContent, ScrollTop, {
  classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
  content: Ember.computed.alias('setting'),
  dirty: propertyNotEqual('buffered.value', 'setting.value'),
  validationMessage: null,

  preview: function() {
    const preview = this.get('setting.preview');
    if (preview) {
      return new Handlebars.SafeString("
" +
                                        preview.replace(/\{\{value\}\}/g, this.get('buffered.value')) +
                                        "
");
    }
  }.property('buffered.value'),

  @computed('partialType')
  typeClass() {
    return this.get('partialType').replace("_", "-");
  },

  @computed('buffered.value')
  enabled: {
    get(bufferedValue) {
      if (Ember.isEmpty(bufferedValue)) { return false; }
      return bufferedValue === 'true';
    },
    set(value) {
      this.set('buffered.value', value ? 'true' : 'false');
    }
  },

  settingName: function() {
    return this.get('setting.setting').replace(/\_/g, ' ');
  }.property('setting.setting'),

  partialType: function()  {
    let type = this.get('setting.type');
    return CustomTypes.indexOf(type) !== -1 ? type : 'string';
  }.property('setting.type'),

  partialName: function() {
    return 'admin/templates/site-settings/' + this.get('partialType');
  }.property('partialType'),

  _watchEnterKey: function() {
    const self = this;
    this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
      if (e.keyCode === 13) { // enter key
        self._save();
      }
    });
  }.on('didInsertElement'),

  _removeBindings: function() {
    this.$().off("keydown.site-setting-enter");
  }.on("willDestroyElement"),

  _save() {
    const setting = this.get('buffered');
    const self = this;
    SiteSetting.update(setting.get('setting'), setting.get('value')).then(function() {
      self.set('validationMessage', null);
      self.commitBuffer();
    }).catch(function(e) {
      if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
        self.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
      } else {
        self.set('validationMessage', I18n.t('generic_error'));
      }
    });
  },

  actions: {
    save() {
      this._save();
    },

    resetDefault() {
      this.set('buffered.value', this.get('setting.default'));
      this._save();
    },

    cancel() {
      this.rollbackBuffer();
    }
  }

});
<commit_msg>FIX: Checkboxes weren't always being set properly.<commit_after>import BufferedContent from 'discourse/mixins/buffered-content';
import ScrollTop from 'discourse/mixins/scroll-top';
import SiteSetting from 'admin/models/site-setting';
import { propertyNotEqual } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';

const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list'];

export default Ember.Component.extend(BufferedContent, ScrollTop, {
  classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
  content: Ember.computed.alias('setting'),
  dirty: propertyNotEqual('buffered.value', 'setting.value'),
  validationMessage: null,

  preview: function() {
    const preview = this.get('setting.preview');
    if (preview) {
      return new Handlebars.SafeString("
" +
                                        preview.replace(/\{\{value\}\}/g, this.get('buffered.value')) +
                                        "
");
    }
  }.property('buffered.value'),

  @computed('partialType')
  typeClass() {
    return this.get('partialType').replace("_", "-");
  },

  @computed('buffered.value')
  enabled: {
    get(bufferedValue) {
      if (Ember.isEmpty(bufferedValue)) { return false; }
      return bufferedValue === 'true';
    },
    set(value) {
      this.set('buffered.value', value ? 'true' : 'false');
      return value;
    }
  },

  settingName: function() {
    return this.get('setting.setting').replace(/\_/g, ' ');
  }.property('setting.setting'),

  partialType: function()  {
    let type = this.get('setting.type');
    return CustomTypes.indexOf(type) !== -1 ? type : 'string';
  }.property('setting.type'),

  partialName: function() {
    return 'admin/templates/site-settings/' + this.get('partialType');
  }.property('partialType'),

  _watchEnterKey: function() {
    const self = this;
    this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
      if (e.keyCode === 13) { // enter key
        self._save();
      }
    });
  }.on('didInsertElement'),

  _removeBindings: function() {
    this.$().off("keydown.site-setting-enter");
  }.on("willDestroyElement"),

  _save() {
    const setting = this.get('buffered');
    const self = this;
    SiteSetting.update(setting.get('setting'), setting.get('value')).then(function() {
      self.set('validationMessage', null);
      self.commitBuffer();
    }).catch(function(e) {
      if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
        self.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
      } else {
        self.set('validationMessage', I18n.t('generic_error'));
      }
    });
  },

  actions: {
    save() {
      this._save();
    },

    resetDefault() {
      this.set('buffered.value', this.get('setting.default'));
      this._save();
    },

    cancel() {
      this.rollbackBuffer();
    }
  }

});
<|endoftext|>