<commit_before>const net = require('net');
const express = require('express');
const bodyParser = require('body-parser');
const randomString = require('randomstring');
const mondogbAdapter = require('./adapter/mongodb');
const MomentumEventEmitter = require('./event/emitter');
const adapters = {
    mongodb: mondogbAdapter
};
const getAdapter = (...args) => {
    const names = Object.keys(adapters);
    while (names.length) {
        const name = names.shift();
        const AdapterClass = adapters[name];
        if (AdapterClass.isCompatible(...args)) {
            return new AdapterClass(...args);
        }
    }

    return null;
};
const getIpFromRequest = request => {
    return request.headers['x-forwarded-for'] || request.connection.remoteAddress;
};
const eventTypes = {
    updateCollection: 'update-collection',
    updateItem: 'update-item',
    removeCollection: 'remove-collection',
    removeItem: 'remove-item',
    insert: 'insert'
};

class Momentum {
    constructor(...args) {
        this.isReady = false;
        this.readyPromises = [];
        this.filters = {};
        this.options = {
            maxTokensPerIp: 16,
            timeOut: 120000,
            collectionPrefix: 'mm_'
        };
        this.adapter = getAdapter(...args);
    }

    static addAdapter(name, adapter) {
        adapters[name] = adapter;
    }

    static connect(app, ...args) {
        const momentum = new Momentum(...args);

        return momentum.start(app).then(() => {
            return momentum;
        });
    }

    addFilter(filter, callback) {
        this.filters[filter] = callback;
    }

    getFilter(filter) {
        return this.filters[filter] || null;
    }

    getAuthorizationStrategy() {
        return this.authorizationStrategy || (() => {
            return new Promise(resolve => {
                resolve(true);
            });
        });
    }

    setAuthorizationStrategy(authorizationStrategy) {
        this.authorizationStrategy = authorizationStrategy;

        return this;
    }

    isAllowed(mode, method, args, request, response) {
        return this.getAuthorizationStrategy()(mode, method, args, request, response);
    }

    setUrlPrefix(urlPrefix) {
        this.urlPrefix = urlPrefix;
    }

    getUrlPrefix() {
        return this.urlPrefix || '/api/mm/';
    }

    linkApplication(app) {
        this.linkedApp = app;
    }

    setApplicationPort(appPort) {
        this.appPort = appPort;
    }

    invalidateTokens(filter) {
        const tokens = this.options.collectionPrefix + 'tokens';
        return this.remove(tokens, filter);
    }

    isTokenValid(token) {
        const tokens = this.options.collectionPrefix + 'tokens';
        return this.count(tokens, {token}).then(count => {
            return count > 0;
        });
    }

    addReadyRoute() {
        this.app.get(this.getUrlPrefix() + 'ready', (request, response) => {
            const readyCallback = () => {
                const ip = getIpFromRequest(request);
                const tokens = this.options.collectionPrefix + 'tokens';
                this.count(tokens, {ip}).then(count => {
                    if (count > this.options.maxTokensPerIp) {
                        response.status(429).json({
                            error: 'Too many connections'
                        });

                        return;
                    }

                    const time = (new Date()).getTime();
                    const start = time - 2 * this.options.timeOut;
                    this.remove(tokens, {updatedAt: {$lt: start}}).then(() => {
                        const token = {
                            token: randomString.generate({
                                charset: 'alphanumeric',
                                length: 24
                            }),
                            updatedAt: time,
                            ip
                        };
                        this.insertOne(tokens, token).then(() => {
                            response.status(200).json({
                                status: 'success',
                                token: token.token
                            });
                        });
                    });
                });
            };
            if (this.isReady) {
                readyCallback();

                return;
            }

            const readyPromise = new Promise(resolve => {
                this.readyPromises.push(resolve);
            });
            readyPromise.then(readyCallback);
        });
    }

    addOnRoute() {
        this.app.get(this.getUrlPrefix() + 'on', (request, response) => {
            let end;
            let timeout = setTimeout(() => {
                response.status(200).json({events: []});
                end();
            }, this.options.timeOut);
            let group = null;
            const token = request.query.token;
            const eventsCollection = this.options.collectionPrefix + 'events';
            const off = this.on('listen:' + token, (...args) => {
                clearTimeout(timeout);
                this.insertOne(eventsCollection, {
                    token,
                    args: JSON.stringify(args)
                }).then(() => {
                    if (!group) {
                        group = setTimeout(() => {
                            this.find(eventsCollection, {token}).toArray((err, events) => {
                                if (!err && events && !response.headerSent) {
                                    response.status(200).json({
                                        events: events.map(event => {
                                            event.args = JSON.parse(event.args);

                                            return event;
                                        })
                                    });
                                    end();
                                    events.forEach(event => {
                                        this.remove(eventsCollection, this.getFilterFromItemId(event));
                                    });
                                }
                            });
                        }, 200);
                    }
                });
            });
            end = () => {
                setTimeout(off, this.options.timeOut / 4);
            };
        });
    }

    addListenRoute() {
        this.app.post(this.getUrlPrefix() + 'listen', bodyParser.json(), (request, response) => {
            const token = request.body.token;
            const collection = request.body.collection;
            if (!collection) {
                response.status(400).json({
                    error: 'Missing collection name'
                });

                return;
            }
            this.isTokenValid(token).then(valid => {
                if (!valid) {
                    response.status(500).json({
                        error: 'Invalid token ' + token
                    });

                    return;
                }

                const filter = request.body.filter;
                let handler;
                if (filter) {
                    handler = this.getFilter(filter);
                    if (!handler) {
                        response.status(400).json({
                            error: 'Unknown filter ' + filter
                        });

                        return;
                    }
                }
                let off;
                const listener = (...args) => {
                    if (!filter) {
                        this.emit('listen:' + token, ...args);

                        return;
                    }

                    handler(args).then(args => {
                        this.emit('listen:' + token, ...args);
                    });
                };
                const check = () => {
                    setTimeout(() => {
                        this.isTokenValid(token).then(valid => {
                            (valid ? check : off)();
                        });
                    }, this.options.timeOut);
                };
                check();
                const id = request.body.id;
                if (id) {
                    off = this.onItemTouched(collection, id, listener);
                    response.status(200).json({status: 'success'});

                    return;
                }

                off = this.onCollectionTouched(collection, listener);
                response.status(200).json({status: 'success'});
            });
        });
    }

    addEmitRoute() {
        this.app.post(this.getUrlPrefix() + 'emit', bodyParser.json(), (request, response) => {
            const method = request.body.method;
            const args = request.body.args;
            if (['insertOne', 'updateOne', 'remove'].indexOf(method) === -1) {
                response.status(400).json({
                    error: method + ' method unknown'
                });

                return;
            }

            if (!this.isAllowed('emit', method, args, request, response)) {
                response.status(403).json({
                    error: method + ' not allowed with ' + JSON.stringify(args)
                });

                return;
            }

            this[method](...args).then(result => {
                response.status(200).json(result);
            }).catch(error => {
                response.status(500).json({error});
            });
        });
    }

    start(app = null) {
        let appPort = null;
        if (!isNaN(app)) {
            appPort = app;
            app = null;
        }
        this.stop();
        this.setApplicationPort(appPort);
        this.linkApplication(app);
        this.app = this.linkedApp || (() => {
            const expressApp = express();
            this.server = expressApp.listen(this.appPort);

            return expressApp;
        })();
        this.addReadyRoute();
        this.addOnRoute();
        this.addListenRoute();
        this.addEmitRoute();
        this.events = new MomentumEventEmitter();
        const start = this.adapter.start();
        start.then(() => {
            this.isReady = true;
            this.readyPromises.forEach(promise => {
                promise();
            });
            this.readyPromises = [];
        });

        return start;
    }

    stop() {
        if (this.adapter) {
            this.adapter.stop();
        }
        if (this.server) {
            this.server.close();
        }
    }

    on(events, ...args) {
        if (!events) {
            throw new Error('event must be a string or an array');
        }
        if (!events.forEach) {
            events = [events];
        }
        events.forEach(event => {
            this.events.on(event, ...args);
        });

        return () => {
            events.forEach(event => {
                this.events.removeListener(event, ...args);
            });
        };
    }

    onEvent(eventKey, eventParam, args) {
        return this.on(eventTypes[eventKey] + ':' + eventParam, ...args);
    }

    onCollectionTouched(collection, ...args) {
        const offCollectionUpdate = this.onCollectionUpdate(collection, ...args);
        const offCollectionRemove = this.onCollectionRemove(collection, ...args);
        const offInsert = this.onInsert(collection, ...args);

        return () => {
            offCollectionUpdate();
            offCollectionRemove();
            offInsert();
        };
    }

    onItemTouched(collection, item, ...args) {
        const offItemRemove = this.onItemRemove(collection, item, ...args);
        const offItemUpdate = this.onItemUpdate(collection, item, ...args);

        return () => {
            offItemRemove();
            offItemUpdate();
        };
    }

    onCollectionUpdate(collection, ...args) {
        return this.onEvent('updateCollection', collection, args);
    }

    onItemUpdate(collection, item, ...args) {
        return this.onEvent('updateItem', collection + ':' + item, args);
    }

    onCollectionRemove(collection, ...args) {
        return this.onEvent('removeCollection', collection, args);
    }

    onItemRemove(collection, item, ...args) {
        return this.onEvent('removeItem', collection + ':' + item, args);
    }

    onInsert(collection, ...args) {
        return this.onEvent('insert', collection, args);
    }

    emit(...args) {
        return this.events.emit(...args);
    }

    emitEvent(eventKey, eventParam, ...args) {
        const event = eventTypes[eventKey];

        return this.emit(event + ':' + eventParam, event, ...args);
    }

    remove(collection, filter, options) {
        return new Promise((resolve, reject) => {
            this.find(collection, filter).toArray((err, objs) => {
                if (err) {
                    reject(err);

                    return;
                }

                const ids = objs.map(obj => this.getItemId(obj));
                const promise = this.adapter.remove(collection, filter, options);
                promise.then(result => {
                    const args = ['remove', collection, ids, filter, result];
                    this.emitEvent('removeCollection', collection, ...args);
                    ids.forEach(id => {
                        const itemArgs = args.slice();
                        itemArgs[2] = id;
                        this.emitEvent('removeItem', collection + ':' + id, ...itemArgs);
                    });
                });

                resolve(promise);
            });
        });
    }

    insertOne(collection, document, options) {
        const promise = this.adapter.insertOne(collection, document, options);
        promise.then(result => {
            const args = ['insertOne', collection, document, result];
            this.emitEvent('insert', collection, ...args);
        });

        return promise;
    }

    updateOne(collection, filter, update, options) {
        return this.findOne(collection, filter).then(obj => {
            const id = this.getItemId(obj);
            const promise = this.adapter.updateOne(collection, filter, update, options);
            promise.then(result => {
                const args = ['updateOne', collection, obj, id, filter, update, result];
                this.emitEvent('updateCollection', collection, ...args);
                this.emitEvent('updateItem', collection + ':' + id, ...args);
            });

            return promise;
        });
    }

    getItemId(item) {
        return this.adapter.getItemId(item);
    }

    getFilterFromItemId(item) {
        return this.adapter.getFilterFromItemId(item);
    }

    count(...args) {
        return this.adapter.count(...args);
    }

    find(...args) {
        return this.adapter.find(...args);
    }

    findOne(...args) {
        return this.adapter.findOne(...args);
    }
}

module.exports = Momentum;
<commit_msg>Format code<commit_after>const net = require('net');
const express = require('express');
const bodyParser = require('body-parser');
const randomString = require('randomstring');
const mondogbAdapter = require('./adapter/mongodb');
const MomentumEventEmitter = require('./event/emitter');
const adapters = {
    mongodb: mondogbAdapter
};
const getAdapter = (...args) => {
    const names = Object.keys(adapters);
    while (names.length) {
        const name = names.shift();
        const AdapterClass = adapters[name];
        if (AdapterClass.isCompatible(...args)) {
            return new AdapterClass(...args);
        }
    }

    return null;
};
const getIpFromRequest = request => {
    return request.headers['x-forwarded-for'] || request.connection.remoteAddress;
};
const eventTypes = {
    updateCollection: 'update-collection',
    updateItem: 'update-item',
    removeCollection: 'remove-collection',
    removeItem: 'remove-item',
    insert: 'insert'
};

class Momentum {
    constructor(...args) {
        this.isReady = false;
        this.readyPromises = [];
        this.filters = {};
        this.options = {
            maxTokensPerIp: 16,
            timeOut: 120000,
            collectionPrefix: 'mm_'
        };
        this.adapter = getAdapter(...args);
    }

    static addAdapter(name, adapter) {
        adapters[name] = adapter;
    }

    static connect(app, ...args) {
        const momentum = new Momentum(...args);

        return momentum.start(app).then(() => {
            return momentum;
        });
    }

    addFilter(filter, callback) {
        this.filters[filter] = callback;
    }

    getFilter(filter) {
        return this.filters[filter] || null;
    }

    getAuthorizationStrategy() {
        return this.authorizationStrategy || (() => {
            return new Promise(resolve => {
                resolve(true);
            });
        });
    }

    setAuthorizationStrategy(authorizationStrategy) {
        this.authorizationStrategy = authorizationStrategy;

        return this;
    }

    isAllowed(mode, method, args, request, response) {
        return this.getAuthorizationStrategy()(mode, method, args, request, response);
    }

    setUrlPrefix(urlPrefix) {
        this.urlPrefix = urlPrefix;
    }

    getUrlPrefix() {
        return this.urlPrefix || '/api/mm/';
    }

    linkApplication(app) {
        this.linkedApp = app;
    }

    setApplicationPort(appPort) {
        this.appPort = appPort;
    }

    invalidateTokens(filter) {
        const tokens = this.options.collectionPrefix + 'tokens';
        return this.remove(tokens, filter);
    }

    isTokenValid(token) {
        const tokens = this.options.collectionPrefix + 'tokens';
        return this.count(tokens, {token}).then(count => {
            return count > 0;
        });
    }

    addReadyRoute() {
        this.app.get(this.getUrlPrefix() + 'ready', (request, response) => {
            const readyCallback = () => {
                const ip = getIpFromRequest(request);
                const tokens = this.options.collectionPrefix + 'tokens';
                this.count(tokens, {ip}).then(count => {
                    if (count > this.options.maxTokensPerIp) {
                        response.status(429).json({
                            error: 'Too many connections'
                        });

                        return;
                    }

                    const time = (new Date()).getTime();
                    const start = time - 2 * this.options.timeOut;
                    this.remove(tokens, {updatedAt: {$lt: start}}).then(() => {
                        const token = {
                            token: randomString.generate({
                                charset: 'alphanumeric',
                                length: 24
                            }),
                            updatedAt: time,
                            ip
                        };
                        this.insertOne(tokens, token).then(() => {
                            response.status(200).json({
                                status: 'success',
                                token: token.token
                            });
                        });
                    });
                });
            };
            if (this.isReady) {
                readyCallback();

                return;
            }

            const readyPromise = new Promise(resolve => {
                this.readyPromises.push(resolve);
            });
            readyPromise.then(readyCallback);
        });
    }

    addOnRoute() {
        this.app.get(this.getUrlPrefix() + 'on', (request, response) => {
            let end;
            let timeout = setTimeout(() => {
                response.status(200).json({events: []});
                end();
            }, this.options.timeOut);
            let group = null;
            const token = request.query.token;
            const eventsCollection = this.options.collectionPrefix + 'events';
            const off = this.on('listen:' + token, (...args) => {
                clearTimeout(timeout);
                this.insertOne(eventsCollection, {
                    token,
                    args: JSON.stringify(args)
                }).then(() => {
                    if (!group) {
                        group = setTimeout(() => {
                            this.find(eventsCollection, {token}).toArray((err, events) => {
                                if (!err && events && !response.headerSent) {
                                    response.status(200).json({
                                        events: events.map(event => {
                                            event.args = JSON.parse(event.args);

                                            return event;
                                        })
                                    });
                                    end();
                                    events.forEach(event => {
                                        this.remove(
                                            eventsCollection,
                                            this.getFilterFromItemId(this.getItemId(event))
                                        );
                                    });
                                }
                            });
                        }, 200);
                    }
                });
            });
            end = () => {
                setTimeout(off, this.options.timeOut / 4);
            };
        });
    }

    addListenRoute() {
        this.app.post(this.getUrlPrefix() + 'listen', bodyParser.json(), (request, response) => {
            const token = request.body.token;
            const collection = request.body.collection;
            if (!collection) {
                response.status(400).json({
                    error: 'Missing collection name'
                });

                return;
            }
            this.isTokenValid(token).then(valid => {
                if (!valid) {
                    response.status(500).json({
                        error: 'Invalid token ' + token
                    });

                    return;
                }

                const filter = request.body.filter;
                let handler;
                if (filter) {
                    handler = this.getFilter(filter);
                    if (!handler) {
                        response.status(400).json({
                            error: 'Unknown filter ' + filter
                        });

                        return;
                    }
                }
                let off;
                const listener = (...args) => {
                    if (!filter) {
                        this.emit('listen:' + token, ...args);

                        return;
                    }

                    handler(args).then(args => {
                        this.emit('listen:' + token, ...args);
                    });
                };
                const check = () => {
                    setTimeout(() => {
                        this.isTokenValid(token).then(valid => {
                            (valid ? check : off)();
                        });
                    }, this.options.timeOut);
                };
                check();
                const id = request.body.id;
                if (id) {
                    off = this.onItemTouched(collection, id, listener);
                    response.status(200).json({status: 'success'});

                    return;
                }

                off = this.onCollectionTouched(collection, listener);
                response.status(200).json({status: 'success'});
            });
        });
    }

    addEmitRoute() {
        this.app.post(this.getUrlPrefix() + 'emit', bodyParser.json(), (request, response) => {
            const method = request.body.method;
            const args = request.body.args;
            if (['insertOne', 'updateOne', 'remove'].indexOf(method) === -1) {
                response.status(400).json({
                    error: method + ' method unknown'
                });

                return;
            }

            if (!this.isAllowed('emit', method, args, request, response)) {
                response.status(403).json({
                    error: method + ' not allowed with ' + JSON.stringify(args)
                });

                return;
            }

            this[method](...args).then(result => {
                response.status(200).json(result);
            }).catch(error => {
                response.status(500).json({error});
            });
        });
    }

    start(app = null) {
        let appPort = null;
        if (!isNaN(app)) {
            appPort = app;
            app = null;
        }
        this.stop();
        this.setApplicationPort(appPort);
        this.linkApplication(app);
        this.app = this.linkedApp || (() => {
            const expressApp = express();
            this.server = expressApp.listen(this.appPort);

            return expressApp;
        })();
        this.addReadyRoute();
        this.addOnRoute();
        this.addListenRoute();
        this.addEmitRoute();
        this.events = new MomentumEventEmitter();
        const start = this.adapter.start();
        start.then(() => {
            this.isReady = true;
            this.readyPromises.forEach(promise => {
                promise();
            });
            this.readyPromises = [];
        });

        return start;
    }

    stop() {
        if (this.adapter) {
            this.adapter.stop();
        }
        if (this.server) {
            this.server.close();
        }
    }

    on(events, ...args) {
        if (!events) {
            throw new Error('event must be a string or an array');
        }
        if (!events.forEach) {
            events = [events];
        }
        events.forEach(event => {
            this.events.on(event, ...args);
        });

        return () => {
            events.forEach(event => {
                this.events.removeListener(event, ...args);
            });
        };
    }

    onEvent(eventKey, eventParam, args) {
        return this.on(eventTypes[eventKey] + ':' + eventParam, ...args);
    }

    onCollectionTouched(collection, ...args) {
        const offCollectionUpdate = this.onCollectionUpdate(collection, ...args);
        const offCollectionRemove = this.onCollectionRemove(collection, ...args);
        const offInsert = this.onInsert(collection, ...args);

        return () => {
            offCollectionUpdate();
            offCollectionRemove();
            offInsert();
        };
    }

    onItemTouched(collection, item, ...args) {
        const offItemRemove = this.onItemRemove(collection, item, ...args);
        const offItemUpdate = this.onItemUpdate(collection, item, ...args);

        return () => {
            offItemRemove();
            offItemUpdate();
        };
    }

    onCollectionUpdate(collection, ...args) {
        return this.onEvent('updateCollection', collection, args);
    }

    onItemUpdate(collection, item, ...args) {
        return this.onEvent('updateItem', collection + ':' + item, args);
    }

    onCollectionRemove(collection, ...args) {
        return this.onEvent('removeCollection', collection, args);
    }

    onItemRemove(collection, item, ...args) {
        return this.onEvent('removeItem', collection + ':' + item, args);
    }

    onInsert(collection, ...args) {
        return this.onEvent('insert', collection, args);
    }

    emit(...args) {
        return this.events.emit(...args);
    }

    emitEvent(eventKey, eventParam, ...args) {
        const event = eventTypes[eventKey];

        return this.emit(event + ':' + eventParam, event, ...args);
    }

    remove(collection, filter, options) {
        return new Promise((resolve, reject) => {
            this.find(collection, filter).toArray((err, objs) => {
                if (err) {
                    reject(err);

                    return;
                }

                const ids = objs.map(obj => this.getItemId(obj));
                const promise = this.adapter.remove(collection, filter, options);
                promise.then(result => {
                    const args = ['remove', collection, ids, filter, result];
                    this.emitEvent('removeCollection', collection, ...args);
                    ids.forEach(id => {
                        const itemArgs = args.slice();
                        itemArgs[2] = id;
                        this.emitEvent('removeItem', collection + ':' + id, ...itemArgs);
                    });
                });

                resolve(promise);
            });
        });
    }

    insertOne(collection, document, options) {
        const promise = this.adapter.insertOne(collection, document, options);
        promise.then(result => {
            const args = ['insertOne', collection, document, result];
            this.emitEvent('insert', collection, ...args);
        });

        return promise;
    }

    updateOne(collection, filter, update, options) {
        return this.findOne(collection, filter).then(obj => {
            const id = this.getItemId(obj);
            const promise = this.adapter.updateOne(collection, filter, update, options);
            promise.then(result => {
                const args = ['updateOne', collection, obj, id, filter, update, result];
                this.emitEvent('updateCollection', collection, ...args);
                this.emitEvent('updateItem', collection + ':' + id, ...args);
            });

            return promise;
        });
    }

    getItemId(item) {
        return this.adapter.getItemId(item);
    }

    getFilterFromItemId(item) {
        return this.adapter.getFilterFromItemId(item);
    }

    count(...args) {
        return this.adapter.count(...args);
    }

    find(...args) {
        return this.adapter.find(...args);
    }

    findOne(...args) {
        return this.adapter.findOne(...args);
    }
}

module.exports = Momentum;
<|endoftext|>


<commit_before>var request = require('request')
var asciify = require('asciify')
var fs = require('fs')

var messages = require('./messages.json')
var addContributor = require('./contributors.js')

var baseURL = 'https://api.github.com/repos/jlord/patchwork/'
var stats = {}

// When a new, open Pull Request comes in via the webhook set on jlord/patchwork
// the request is queued and one by one sent here to verify the PR is a part of
// the Git-it challenges (and not a real, other one) and to verify the file
// contents of the PR and merge, making comments when needed.
// called by:
// mergePR(pullreq, function(err, message) { if (err) console.log(new Date(), message, err)
  // callback(err)
// })

module.exports = function(pullreq, callback) {

  if (pullreq.pull_request) pullreq = pullreq.pull_request
  // if branch name doesn't include username, it may be
  // a non git-it related, normal PR
  if (!pullreq.head.ref.toLowerCase().match(pullreq.user.login.toLowerCase()) && pullreq.user.login != "reporobot") {
    // TO DO get username here and use it in the message
    var message = "**Hello! The name of the branch with this Pull Request is not of the `add-USERNAME` pattern so I'm skipping reviewing it for the Git-it challenges.**\nIf you are completing the Git-it challenges, you'll need to close this Pull Request, rename your branch, push it to your fork of Patchwork on GitHub and then start a new Pull Request. Here's how to rename a branch and push it to GitHub from terminal:\n```bash\n$ git branch -m add-USERNAME\n$ git push origin add-USERNAME\n```\n_Make sure to replace USERNAME with your actual GitHub username, with capitals exactly as they apepar on GitHub_."
    return writeComment(message, pullreq.number)
  }

  stats.prNum = pullreq.number

  var options = {
      url: baseURL +'pulls/' + stats.prNum,
      json: true,
      headers: { 'User-Agent': 'request',
                 'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
      }
  }

  function getTime(error, response, body) {
    if (error) return callback(error, "Error in request on PR via number")
    // if a test pr is coming in from @RR
    if (!error && response.statusCode == 200 && pullreq.user.login === "reporobot") {
      var info = body
      stats.time = info.created_at.toLocaleString()
      // RR is PRing on behalf of:
      stats.username = info.head.user.login
      console.log(new Date(), "PR " ,  stats.prNum , "Reporobot Pull Request on behalf of " , stats.username)
      return getFile(stats.prNum)
    }

    if (!error && response.statusCode == 200 && pullreq.user.login != "reporobot") {
      var info = body
      stats.time = info.created_at
      stats.username = info.user.login
      return getFile(stats.prNum)
    }

    callback(body)
  }

  request(options, getTime)

  function getFile(prNum) {
    var options = {
        url: baseURL + 'pulls/' + prNum + '/files',
        json: true,
        headers: {
            'User-Agent': 'request',
            'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
        }
    }

    request(options, function returnFiles(error, response, body) {
      if (error || body.length === 0) return callback(error, "Error finding file in PR")

      if (!error && response.statusCode == 200) {
        if (body.length > 1) {
          console.log(new Date(), "PR " , stats.prNum , "MORE THAN ONE FILE " , stats.username)
          var message = '**Uh oh, I see too many files, there should be one:** `contributors/add-' + stats.username + '`.\n\n- Delete the extra file on your computer.\n- Add and commit that change with these commands in terminal:\n```bash\n$ git add -A && git commit -m "delete extra file"\n```\n- Then push those changes to your branch:\n```bash\n$ git push origin add-' + stats.username + '\n```\n- Check back here to see if it was merged.'
          return writeComment(message, stats.prNum)
        }

        var prInfo = body[0]

        if (prInfo === undefined ) {
          console.log(new Date(), "PR " , stats.prNum , "FILE IS EMPTY " , stats.username)
          var message = "File is empty, try again!"
          return writeComment(message, stats.prNum)
        }

        return verifyFilename(prInfo)
      }
      // huh? why sending this back to function(err, message)?
      callback(body)
    })
  }

  function verifyFilename(prInfo) {
    var filename = prInfo.filename.toLowerCase()
    if (filename.match('contributors/add-' + stats.username.toLowerCase() + '.txt')) {
      console.log(new Date(), "PR " , stats.prNum , "Filename: MATCH " , stats.username)
      return verifyContent(prInfo)
    }
    else {
      var message = 'Filename is different than expected: contributors/add-' + stats.username + '.txt. Close the pull request, rename your file or move it into the `contributors/` folder and try again!'
      return writeComment(message, stats.prNum)
    }
  }

  function verifyContent(prInfo) {
    // pull out the actual pr content
    var patchArray = prInfo.patch.split('@@')
    var patch = patchArray.pop()
    // generate the expected content
    asciify(stats.username, {font:'isometric2'}, function(err, res){
      if (err) return callback(err, "Error generating ascii art to test against")
      console.log(patch)
      console.log(res)
      if (patch !== stats.username) {
        stats.userArt = res
        console.log(new Date(), "PR " , stats.prNum , "Content: MATCH " , stats.username)
        return setTimeout(mergePR(stats.prNum), 5000)
      }
      else {
        var message = "Ascii art wasn't as expected, did something change? Close and re-open please."
        return writeComment(message, stats.prNum)
      }
    })
  }

  function writeComment(message, prNum) {
    stats.username = stats.username || "a skipped PR"
    console.log(new Date(), "PR " +  prNum + "Uh oh, writing comment for " + stats.username)
     var options = {
        url: baseURL + 'issues/' + prNum + '/comments',
        headers: {
            'User-Agent': 'request',
            'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
        },
        json: {'body': message}
    }

    request.post(options, function doneWriteComment(error, response, body) {
      if (error) return callback(error, "Error writing comment on PR")
      callback()
    })
  }

  function mergePR(prNum) {
    var tries = 0
    var limit = 25

    tryMerge()

    function tryMerge() {
      var message = "Merging PR from @" + stats.username
      var options = {
            url: baseURL + 'pulls/' + prNum + '/merge',
            headers: {
                'User-Agent': 'request',
                'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
            },
           json: {'commit_message': message}
       }

        request.put(options, function doneMerge(error, response, body) {
          if (error) return callback(error, "Error merging PR")
          if (response.statusCode != 200) {
            console.log(new Date(), prNum, "ERROR MERGING", response.statusCode, body.message)
            console.log(new Date(), prNum, "TRYING AGAIN")
            if (tries <= limit) {
              tries++
              return setTimeout(tryMerge(prNum), 3000)
            } else {
              callback(null, new Date() + "Could not merge after " + limit + " tries " + prNum)
            }
          }
          if (!error && response.statusCode == 200) {
            console.log(new Date(), "PR " , prNum , "MERGED" , stats.username)
            // add contributor to file and then rebuild page
            return addContributor(stats, callback)
          } else {
           callback(error, body)
          }
        })
      }
    }
}
<commit_msg>set vars earlier<commit_after>var request = require('request')
var asciify = require('asciify')
var fs = require('fs')

var messages = require('./messages.json')
var addContributor = require('./contributors.js')

var baseURL = 'https://api.github.com/repos/jlord/patchwork/'
var stats = {}

// When a new, open Pull Request comes in via the webhook set on jlord/patchwork
// the request is queued and one by one sent here to verify the PR is a part of
// the Git-it challenges (and not a real, other one) and to verify the file
// contents of the PR and merge, making comments when needed.
// called by:
// mergePR(pullreq, function(err, message) { if (err) console.log(new Date(), message, err)
  // callback(err)
// })

module.exports = function(pullreq, callback) {

  if (pullreq.pull_request) pullreq = pullreq.pull_request
  var prBranch = pullreq.head.ref.toLowerCase()
  stats.user = pullreq.user.login
  stats.prNum = pullreq.number

  // if branch name doesn't include username, it may be
  // a non git-it related, normal PR
  if (!pullreq.head.ref.toLowerCase().match(pullreq.user.login.toLowerCase()) && pullreq.user.login != "reporobot") {
    // TO DO get username here and use it in the message
    var message = "**Hello! The name of the branch with this Pull Request is not of the `add-USERNAME` pattern so I'm skipping reviewing it for the Git-it challenges.**\nIf you are completing the Git-it challenges, you'll need to close this Pull Request, rename your branch, push it to your fork of Patchwork on GitHub and then start a new Pull Request. Here's how to rename a branch and push it to GitHub from terminal:\n```bash\n$ git branch -m add-USERNAME\n$ git push origin add-USERNAME\n```\n_Make sure to replace USERNAME with your actual GitHub username, with capitals exactly as they apepar on GitHub_."
    return writeComment(message, pullreq.number)
  }

  stats.prNum = pullreq.number

  var options = {
      url: baseURL +'pulls/' + stats.prNum,
      json: true,
      headers: { 'User-Agent': 'request',
                 'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
      }
  }

  function getTime(error, response, body) {
    if (error) return callback(error, "Error in request on PR via number")
    // if a test pr is coming in from @RR
    if (!error && response.statusCode == 200 && pullreq.user.login === "reporobot") {
      var info = body
      stats.time = info.created_at.toLocaleString()
      // RR is PRing on behalf of:
      stats.username = info.head.user.login
      console.log(new Date(), "PR " ,  stats.prNum , "Reporobot Pull Request on behalf of " , stats.username)
      return getFile(stats.prNum)
    }

    if (!error && response.statusCode == 200 && pullreq.user.login != "reporobot") {
      var info = body
      stats.time = info.created_at
      stats.username = info.user.login
      return getFile(stats.prNum)
    }

    callback(body)
  }

  request(options, getTime)

  function getFile(prNum) {
    var options = {
        url: baseURL + 'pulls/' + prNum + '/files',
        json: true,
        headers: {
            'User-Agent': 'request',
            'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
        }
    }

    request(options, function returnFiles(error, response, body) {
      if (error || body.length === 0) return callback(error, "Error finding file in PR")

      if (!error && response.statusCode == 200) {
        if (body.length > 1) {
          console.log(new Date(), "PR " , stats.prNum , "MORE THAN ONE FILE " , stats.username)
          var message = '**Uh oh, I see too many files, there should be one:** `contributors/add-' + stats.username + '`.\n\n- Delete the extra file on your computer.\n- Add and commit that change with these commands in terminal:\n```bash\n$ git add -A && git commit -m "delete extra file"\n```\n- Then push those changes to your branch:\n```bash\n$ git push origin add-' + stats.username + '\n```\n- Check back here to see if it was merged.'
          return writeComment(message, stats.prNum)
        }

        var prInfo = body[0]

        if (prInfo === undefined ) {
          console.log(new Date(), "PR " , stats.prNum , "FILE IS EMPTY " , stats.username)
          var message = "File is empty, try again!"
          return writeComment(message, stats.prNum)
        }

        return verifyFilename(prInfo)
      }
      // huh? why sending this back to function(err, message)?
      callback(body)
    })
  }

  function verifyFilename(prInfo) {
    var filename = prInfo.filename.toLowerCase()
    if (filename.match('contributors/add-' + stats.username.toLowerCase() + '.txt')) {
      console.log(new Date(), "PR " , stats.prNum , "Filename: MATCH " , stats.username)
      return verifyContent(prInfo)
    }
    else {
      var message = 'Filename is different than expected: contributors/add-' + stats.username + '.txt. Close the pull request, rename your file or move it into the `contributors/` folder and try again!'
      return writeComment(message, stats.prNum)
    }
  }

  function verifyContent(prInfo) {
    // pull out the actual pr content
    var patchArray = prInfo.patch.split('@@')
    var patch = patchArray.pop()
    // generate the expected content
    asciify(stats.username, {font:'isometric2'}, function(err, res){
      if (err) return callback(err, "Error generating ascii art to test against")
      console.log(patch)
      console.log(res)
      if (patch !== stats.username) {
        stats.userArt = res
        console.log(new Date(), "PR " , stats.prNum , "Content: MATCH " , stats.username)
        return setTimeout(mergePR(stats.prNum), 5000)
      }
      else {
        var message = "Ascii art wasn't as expected, did something change? Close and re-open please."
        return writeComment(message, stats.prNum)
      }
    })
  }

  function writeComment(message, prNum) {
    stats.username = stats.username || "a skipped PR"
    console.log(new Date(), "PR " +  prNum + "Uh oh, writing comment for " + stats.username)
     var options = {
        url: baseURL + 'issues/' + prNum + '/comments',
        headers: {
            'User-Agent': 'request',
            'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
        },
        json: {'body': message}
    }

    request.post(options, function doneWriteComment(error, response, body) {
      if (error) return callback(error, "Error writing comment on PR")
      callback()
    })
  }

  function mergePR(prNum) {
    var tries = 0
    var limit = 25

    tryMerge()

    function tryMerge() {
      var message = "Merging PR from @" + stats.username
      var options = {
            url: baseURL + 'pulls/' + prNum + '/merge',
            headers: {
                'User-Agent': 'request',
                'Authorization': 'token ' + process.env['REPOROBOT_TOKEN']
            },
           json: {'commit_message': message}
       }

        request.put(options, function doneMerge(error, response, body) {
          if (error) return callback(error, "Error merging PR")
          if (response.statusCode != 200) {
            console.log(new Date(), prNum, "ERROR MERGING", response.statusCode, body.message)
            console.log(new Date(), prNum, "TRYING AGAIN")
            if (tries <= limit) {
              tries++
              return setTimeout(tryMerge(prNum), 3000)
            } else {
              callback(null, new Date() + "Could not merge after " + limit + " tries " + prNum)
            }
          }
          if (!error && response.statusCode == 200) {
            console.log(new Date(), "PR " , prNum , "MERGED" , stats.username)
            // add contributor to file and then rebuild page
            return addContributor(stats, callback)
          } else {
           callback(error, body)
          }
        })
      }
    }
}
<|endoftext|>


<commit_before>var iconv = require('iconv-lite');
var zlib = require('zlib');
var MAX_REQ_SIZE = 128 * 1024;
var MAX_RES_SIZE = 256 * 1024;
var TIMEOUT = 10000;
var MAX_LENGTH = 512;
var MIN_LENGTH = 256;
var count = 0;
var ids = [];
var data = {};
var proxy, binded, timeout, interval, util;

function disable() {
	proxy.removeListener('request', handleRequest);
	ids = [];
	data = {};
	interval && clearInterval(interval);
	interval = null;
	binded = false;
}

/**
 * 如果超过最大缓存数，清理如下请求数据：
 * 1. 已经请求结束且结束时间超过10秒
 * 2. 请求#1前面的未结束且未被ui读取过的请求
 */
function clearCache() {
	var len = ids.length;
	if (len <= MAX_LENGTH) {
		return;
	}

	var index = -1; //已经完成，且缓存超过10s的最后一个请求
	var now = Date.now();
	for (var i = len - 1; i >= 0; i--) {
		var curData = data[ids[i]];
		if (curData.endTime && now - curData.endTime > TIMEOUT) {
			index = i;
			break;
		}
	}

	if (index < 0) {
		return;
	}

	var _ids = [];
	var end;
	++index;
	for (var i = 0; i < index; i++) {
		var id = ids[i];
		var curData = data[id];
		if (end || (curData.read && !curData.endTime)) {
			_ids.push(id);
		} else {
			delete data[id];
			end = len-- < MIN_LENGTH;
		}
	}
	ids = _ids.concat(ids.slice(index));
}

function passThrough(chunk, encoding, callback) {
	callback(null, chunk);
}

function decode(body) {
	if (body) {
		var _body = body + '';
		if (_body.indexOf('�') != -1) {
			_body = iconv.decode(body, 'gbk');
		}
		body = _body;
	}

	return body;
}

function get() {
	!binded && proxy.on('request', handleRequest);
	binded = true;
	clearTimeout(timeout);
	timeout = setTimeout(disable, TIMEOUT);
	if (!interval) {
		interval = setInterval(clearCache, 3000);
	}
	return [];
}

function handleRequest(req) {
	var dnsTime = req.dnsTime || 0;
	req.dnsTime = Date.now();
	var startTime = startTime - dnsTime;
	var id = startTime + '-' + ++count;
	var reqData = {
			dnsTime: req.dnsTime,
			method: req.method || 'GET',
			httpVersion: req.httpVersion || '1.1',
            host: req.host || '127.0.0.1',
            headers: req.headers
		};
	var resData = {};

	var curData = data[id] = {
			url: req.url,
			startTime: startTime,
			req: reqData,
			res: resData
	};

	ids.push(id);
	req.on('response', handleResponse);
	req.on('error', function(err) {
		reqData.body = err && err.stack;
		curData.endTime = reqData.requestTime = Date.now();
		req.removeListener('response', handleResponse);
		req._transform = passThrough;
		curData.reqError = true;
	});

	var reqBody;

	if (util.hasRequestBody(req)) {
		reqBody = false;
	}
	req._transform = function(chunk, encoding, callback) {

		if (chunk && (reqBody || reqBody === false)) {
			reqBody = reqBody ? Buffer.concat([reqBody, chunk]) : chunk;
		}

		if (reqBody && reqBody.length > MAX_REQ_SIZE) {
			reqBody = null;
		}

		if (!chunk) {
			reqData.requestTime = Date.now();
			curData.reqEnd = true;
			reqData.body = decode(reqBody);
		}

		callback(null, chunk);
	};

	function handleResponse(res) {
		resData.responseTime = Date.now();
		res.on('error', function(err) {
			resData.body = err && err.stack;
			curData.endTime = Date.now();
			res._transform = passThrough;
			curData.resError = true;
		});

		var resBody;
		var contentType = util.getContentType(res.headers);
		if (contentType && contentType != 'IMG' && util.hasBody(res)) {
			resBody = false;
		}

		res._transform = function(chunk, encoding, callback) {
			if (chunk && (resBody || resBody === false)) {
				resBody = resBody ? Buffer.concat([resBody, chunk]) : chunk;
			}

			if (resBody && resBody.length > MAX_RES_SIZE) {
				resBody = null;
			}

			if (!chunk) {
				curData.endTime = Date.now();
				resData.state = 'close';
				curData.resEnd = true;
				if (resBody) {
					var unzip;
					switch (util.toLowerCase(res.headers['content-encoding'])) {
					    case 'gzip':
					    	unzip = zlib.gunzip.bind(zlib);
					      break;
					    case 'deflate':
					    	unzip = zlib.inflate.bind(zlib);
					      break;
					}

					if (unzip) {
						unzip(resBody, function(err, body) {
							if (err) {
								callback(err, chunk);
								return;
							}
							resData.body = decode(body);
							callback(null, chunk);
						});
						return;
					}
				}
				resData.body = decode(resBody);
			}

			callback(null, chunk);
		};
	}

}

module.exports = function init(_proxy) {
	proxy = _proxy;
	util = proxy.util;
	proxy.on('request', handleRequest);
	binned = true;
	interval = setInterval(clearCache, 3000);
	module.exports = get;
};<commit_msg>clear cache<commit_after>var iconv = require('iconv-lite');
var zlib = require('zlib');
var MAX_REQ_SIZE = 128 * 1024;
var MAX_RES_SIZE = 256 * 1024;
var TIMEOUT = 10000;
var MAX_LENGTH = 512;
var MIN_LENGTH = 256;
var count = 0;
var ids = [];
var data = {};
var proxy, binded, timeout, interval, util;

function disable() {
	proxy.removeListener('request', handleRequest);
	ids = [];
	data = {};
	interval && clearInterval(interval);
	interval = null;
	binded = false;
}

/**
 * 如果超过最大缓存数，清理如下请求数据：
 * 1. 已经请求结束且结束时间超过10秒
 * 2. 请求#1前面的未结束且未被ui读取过的请求
 */
function clearCache() {
	var len = ids.length;
	if (len <= MAX_LENGTH) {
		return;
	}

	var index = -1; //已经完成，且缓存超过10s的最后一个请求
	var now = Date.now();
	for (var i = len - 1; i >= 0; i--) {
		var curData = data[ids[i]];
		if (curData.endTime && now - curData.endTime > TIMEOUT) {
			index = i;
			break;
		}
	}

	if (index < 0) {
		return;
	}

	var _ids = [];
	var end;
	++index;
	for (var i = 0; i < index; i++) {
		var id = ids[i];
		var curData = data[id];
		if (curData.read && !curData.endTime) {
			_ids.push(id);
		} else {
			delete data[id];
			if (len-- < MIN_LENGTH) {
				_ids.push.apply(_ids, ids.slice(i + 1, index));
				break;
			}
		}
	}
	ids = _ids.concat(ids.slice(index));
}

function passThrough(chunk, encoding, callback) {
	callback(null, chunk);
}

function decode(body) {
	if (body) {
		var _body = body + '';
		if (_body.indexOf('�') != -1) {
			_body = iconv.decode(body, 'gbk');
		}
		body = _body;
	}

	return body;
}

function get() {
	!binded && proxy.on('request', handleRequest);
	binded = true;
	clearTimeout(timeout);
	timeout = setTimeout(disable, TIMEOUT);
	if (!interval) {
		interval = setInterval(clearCache, 3000);
	}
	return [];
}

function handleRequest(req) {
	var dnsTime = req.dnsTime || 0;
	req.dnsTime = Date.now();
	var startTime = startTime - dnsTime;
	var id = startTime + '-' + ++count;
	var reqData = {
			dnsTime: req.dnsTime,
			method: req.method || 'GET',
			httpVersion: req.httpVersion || '1.1',
            host: req.host || '127.0.0.1',
            headers: req.headers
		};
	var resData = {};

	var curData = data[id] = {
			url: req.url,
			startTime: startTime,
			req: reqData,
			res: resData
	};

	ids.push(id);
	req.on('response', handleResponse);
	req.on('error', function(err) {
		reqData.body = err && err.stack;
		curData.endTime = reqData.requestTime = Date.now();
		req.removeListener('response', handleResponse);
		req._transform = passThrough;
		curData.reqError = true;
	});

	var reqBody;

	if (util.hasRequestBody(req)) {
		reqBody = false;
	}
	req._transform = function(chunk, encoding, callback) {

		if (chunk && (reqBody || reqBody === false)) {
			reqBody = reqBody ? Buffer.concat([reqBody, chunk]) : chunk;
		}

		if (reqBody && reqBody.length > MAX_REQ_SIZE) {
			reqBody = null;
		}

		if (!chunk) {
			reqData.requestTime = Date.now();
			curData.reqEnd = true;
			reqData.body = decode(reqBody);
		}

		callback(null, chunk);
	};

	function handleResponse(res) {
		resData.responseTime = Date.now();
		res.on('error', function(err) {
			resData.body = err && err.stack;
			curData.endTime = Date.now();
			res._transform = passThrough;
			curData.resError = true;
		});

		var resBody;
		var contentType = util.getContentType(res.headers);
		if (contentType && contentType != 'IMG' && util.hasBody(res)) {
			resBody = false;
		}

		res._transform = function(chunk, encoding, callback) {
			if (chunk && (resBody || resBody === false)) {
				resBody = resBody ? Buffer.concat([resBody, chunk]) : chunk;
			}

			if (resBody && resBody.length > MAX_RES_SIZE) {
				resBody = null;
			}

			if (!chunk) {
				curData.endTime = Date.now();
				resData.state = 'close';
				curData.resEnd = true;
				if (resBody) {
					var unzip;
					switch (util.toLowerCase(res.headers['content-encoding'])) {
					    case 'gzip':
					    	unzip = zlib.gunzip.bind(zlib);
					      break;
					    case 'deflate':
					    	unzip = zlib.inflate.bind(zlib);
					      break;
					}

					if (unzip) {
						unzip(resBody, function(err, body) {
							if (err) {
								callback(err, chunk);
								return;
							}
							resData.body = decode(body);
							callback(null, chunk);
						});
						return;
					}
				}
				resData.body = decode(resBody);
			}

			callback(null, chunk);
		};
	}

}

module.exports = function init(_proxy) {
	proxy = _proxy;
	util = proxy.util;
	proxy.on('request', handleRequest);
	binned = true;
	interval = setInterval(clearCache, 3000);
	module.exports = get;
};<|endoftext|>


<commit_before>'use strict';

import { compareNodeNames, toElement, moveChildren, createElementNS, doc } from './util';
import specialElHandlers from './specialElHandlers';

var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;

function noop() {}

function defaultGetNodeKey(node) {
    return node.id;
}

export default function morphdomFactory(morphAttrs) {

    return function morphdom(fromNode, toNode, options) {
        if (!options) {
            options = {};
        }

        if (typeof toNode === 'string') {
            if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') {
                var toNodeHtml = toNode;
                toNode = doc.createElement('html');
                toNode.innerHTML = toNodeHtml;
            } else {
                toNode = toElement(toNode);
            }
        }

        var getNodeKey = options.getNodeKey || defaultGetNodeKey;
        var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
        var onNodeAdded = options.onNodeAdded || noop;
        var onBeforeElUpdated = options.onBeforeElUpdated || noop;
        var onElUpdated = options.onElUpdated || noop;
        var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
        var onNodeDiscarded = options.onNodeDiscarded || noop;
        var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
        var childrenOnly = options.childrenOnly === true;

        // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
        var fromNodesLookup = {};
        var keyedRemovalList;

        function addKeyedRemoval(key) {
            if (keyedRemovalList) {
                keyedRemovalList.push(key);
            } else {
                keyedRemovalList = [key];
            }
        }

        function walkDiscardedChildNodes(node, skipKeyedNodes) {
            if (node.nodeType === ELEMENT_NODE) {
                var curChild = node.firstChild;
                while (curChild) {

                    var key = undefined;

                    if (skipKeyedNodes && (key = getNodeKey(curChild))) {
                        // If we are skipping keyed nodes then we add the key
                        // to a list so that it can be handled at the very end.
                        addKeyedRemoval(key);
                    } else {
                        // Only report the node as discarded if it is not keyed. We do this because
                        // at the end we loop through all keyed elements that were unmatched
                        // and then discard them in one final pass.
                        onNodeDiscarded(curChild);
                        if (curChild.firstChild) {
                            walkDiscardedChildNodes(curChild, skipKeyedNodes);
                        }
                    }

                    curChild = curChild.nextSibling;
                }
            }
        }

        /**
         * Removes a DOM node out of the original DOM
         *
         * @param  {Node} node The node to remove
         * @param  {Node} parentNode The nodes parent
         * @param  {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
         * @return {undefined}
         */
        function removeNode(node, parentNode, skipKeyedNodes) {
            if (onBeforeNodeDiscarded(node) === false) {
                return;
            }

            if (parentNode) {
                parentNode.removeChild(node);
            }

            onNodeDiscarded(node);
            walkDiscardedChildNodes(node, skipKeyedNodes);
        }

        // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
        // function indexTree(root) {
        //     var treeWalker = document.createTreeWalker(
        //         root,
        //         NodeFilter.SHOW_ELEMENT);
        //
        //     var el;
        //     while((el = treeWalker.nextNode())) {
        //         var key = getNodeKey(el);
        //         if (key) {
        //             fromNodesLookup[key] = el;
        //         }
        //     }
        // }

        // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
        //
        // function indexTree(node) {
        //     var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
        //     var el;
        //     while((el = nodeIterator.nextNode())) {
        //         var key = getNodeKey(el);
        //         if (key) {
        //             fromNodesLookup[key] = el;
        //         }
        //     }
        // }

        function indexTree(node) {
            if (node.nodeType === ELEMENT_NODE) {
                var curChild = node.firstChild;
                while (curChild) {
                    var key = getNodeKey(curChild);
                    if (key) {
                        fromNodesLookup[key] = curChild;
                    }

                    // Walk recursively
                    indexTree(curChild);

                    curChild = curChild.nextSibling;
                }
            }
        }

        indexTree(fromNode);

        function handleNodeAdded(el) {
            onNodeAdded(el);

            var curChild = el.firstChild;
            while (curChild) {
                var nextSibling = curChild.nextSibling;

                var key = getNodeKey(curChild);
                if (key) {
                    var unmatchedFromEl = fromNodesLookup[key];
                    if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
                        curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
                        morphEl(unmatchedFromEl, curChild);
                    }
                }

                handleNodeAdded(curChild);
                curChild = nextSibling;
            }
        }

        function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
            // We have processed all of the "to nodes". If curFromNodeChild is
            // non-null then we still have some from nodes left over that need
            // to be removed
            while (curFromNodeChild) {
                var fromNextSibling = curFromNodeChild.nextSibling;
                if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
                    // Since the node is keyed it might be matched up later so we defer
                    // the actual removal to later
                    addKeyedRemoval(curFromNodeKey);
                } else {
                    // NOTE: we skip nested keyed nodes from being removed since there is
                    //       still a chance they will be matched up later
                    removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                }
                curFromNodeChild = fromNextSibling;
            }
        }

        function morphEl(fromEl, toEl, childrenOnly) {
            var toElKey = getNodeKey(toEl);

            if (toElKey) {
                // If an element with an ID is being morphed then it will be in the final
                // DOM so clear it out of the saved elements collection
                delete fromNodesLookup[toElKey];
            }

            if (toNode.isSameNode && toNode.isSameNode(fromNode)) {
                return;
            }

            if (!childrenOnly) {
                // optional
                if (onBeforeElUpdated(fromEl, toEl) === false) {
                    return;
                }

                // update attributes on original DOM element first
                morphAttrs(fromEl, toEl);
                // optional
                onElUpdated(fromEl);

                if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
                    return;
                }
            }
            if (fromEl.nodeName !== 'TEXTAREA') {
              morphChildren(fromEl, toEl);
            } else {
              specialElHandlers.TEXTAREA(fromEl, toEl);
            }
        }

        function morphChildren(fromEl, toEl) {
            var curToNodeChild = toEl.firstChild;
            var curFromNodeChild = fromEl.firstChild;
            var curToNodeKey;
            var curFromNodeKey;

            var fromNextSibling;
            var toNextSibling;
            var matchingFromEl;

            // walk the children
            outer: while (curToNodeChild) {
                toNextSibling = curToNodeChild.nextSibling;
                curToNodeKey = getNodeKey(curToNodeChild);

                // walk the fromNode children all the way through
                while (curFromNodeChild) {
                    fromNextSibling = curFromNodeChild.nextSibling;

                    if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
                        curToNodeChild = toNextSibling;
                        curFromNodeChild = fromNextSibling;
                        continue outer;
                    }

                    curFromNodeKey = getNodeKey(curFromNodeChild);

                    var curFromNodeType = curFromNodeChild.nodeType;

                    // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
                    var isCompatible = undefined;

                    if (curFromNodeType === curToNodeChild.nodeType) {
                        if (curFromNodeType === ELEMENT_NODE) {
                            // Both nodes being compared are Element nodes

                            if (curToNodeKey) {
                                // The target node has a key so we want to match it up with the correct element
                                // in the original DOM tree
                                if (curToNodeKey !== curFromNodeKey) {
                                    // The current element in the original DOM tree does not have a matching key so
                                    // let's check our lookup to see if there is a matching element in the original
                                    // DOM tree
                                    if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
                                        if (fromNextSibling === matchingFromEl) {
                                            // Special case for single element removals. To avoid removing the original
                                            // DOM node out of the tree (since that can break CSS transitions, etc.),
                                            // we will instead discard the current node and wait until the next
                                            // iteration to properly match up the keyed target element with its matching
                                            // element in the original tree
                                            isCompatible = false;
                                        } else {
                                            // We found a matching keyed element somewhere in the original DOM tree.
                                            // Let's move the original DOM node into the current position and morph
                                            // it.

                                            // NOTE: We use insertBefore instead of replaceChild because we want to go through
                                            // the `removeNode()` function for the node that is being discarded so that
                                            // all lifecycle hooks are correctly invoked
                                            fromEl.insertBefore(matchingFromEl, curFromNodeChild);

                                            // fromNextSibling = curFromNodeChild.nextSibling;

                                            if (curFromNodeKey) {
                                                // Since the node is keyed it might be matched up later so we defer
                                                // the actual removal to later
                                                addKeyedRemoval(curFromNodeKey);
                                            } else {
                                                // NOTE: we skip nested keyed nodes from being removed since there is
                                                //       still a chance they will be matched up later
                                                removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                                            }

                                            curFromNodeChild = matchingFromEl;
                                        }
                                    } else {
                                        // The nodes are not compatible since the "to" node has a key and there
                                        // is no matching keyed node in the source tree
                                        isCompatible = false;
                                    }
                                }
                            } else if (curFromNodeKey) {
                                // The original has a key
                                isCompatible = false;
                            }

                            isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
                            if (isCompatible) {
                                // We found compatible DOM elements so transform
                                // the current "from" node to match the current
                                // target DOM node.
                                // MORPH
                                morphEl(curFromNodeChild, curToNodeChild);
                            }

                        } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
                            // Both nodes being compared are Text or Comment nodes
                            isCompatible = true;
                            // Simply update nodeValue on the original node to
                            // change the text value
                            if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
                                curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
                            }

                        }
                    }

                    if (isCompatible) {
                        // Advance both the "to" child and the "from" child since we found a match
                        // Nothing else to do as we already recursively called morphChildren above
                        curToNodeChild = toNextSibling;
                        curFromNodeChild = fromNextSibling;
                        continue outer;
                    }

                    // No compatible match so remove the old node from the DOM and continue trying to find a
                    // match in the original DOM. However, we only do this if the from node is not keyed
                    // since it is possible that a keyed node might match up with a node somewhere else in the
                    // target tree and we don't want to discard it just yet since it still might find a
                    // home in the final DOM tree. After everything is done we will remove any keyed nodes
                    // that didn't find a home
                    if (curFromNodeKey) {
                        // Since the node is keyed it might be matched up later so we defer
                        // the actual removal to later
                        addKeyedRemoval(curFromNodeKey);
                    } else {
                        // NOTE: we skip nested keyed nodes from being removed since there is
                        //       still a chance they will be matched up later
                        removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                    }

                    curFromNodeChild = fromNextSibling;
                } // END: while(curFromNodeChild) {}

                // If we got this far then we did not find a candidate match for
                // our "to node" and we exhausted all of the children "from"
                // nodes. Therefore, we will just append the current "to" node
                // to the end
                if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
                    fromEl.appendChild(matchingFromEl);
                    // MORPH
                    morphEl(matchingFromEl, curToNodeChild);
                } else {
                    var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
                    if (onBeforeNodeAddedResult !== false) {
                        if (onBeforeNodeAddedResult) {
                            curToNodeChild = onBeforeNodeAddedResult;
                        }

                        if (curToNodeChild.actualize) {
                            curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
                        }
                        fromEl.appendChild(curToNodeChild);
                        handleNodeAdded(curToNodeChild);
                    }
                }

                curToNodeChild = toNextSibling;
                curFromNodeChild = fromNextSibling;
            }

            cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);

            var specialElHandler = specialElHandlers[fromEl.nodeName];
            if (specialElHandler) {
                specialElHandler(fromEl, toEl);
            }
        } // END: morphChildren(...)

        var morphedNode = fromNode;
        var morphedNodeType = morphedNode.nodeType;
        var toNodeType = toNode.nodeType;

        if (!childrenOnly) {
            // Handle the case where we are given two DOM nodes that are not
            // compatible (e.g. <div> --> <span> or <div> --> TEXT)
            if (morphedNodeType === ELEMENT_NODE) {
                if (toNodeType === ELEMENT_NODE) {
                    if (!compareNodeNames(fromNode, toNode)) {
                        onNodeDiscarded(fromNode);
                        morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
                    }
                } else {
                    // Going from an element node to a text node
                    morphedNode = toNode;
                }
            } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
                if (toNodeType === morphedNodeType) {
                    if (morphedNode.nodeValue !== toNode.nodeValue) {
                        morphedNode.nodeValue = toNode.nodeValue;
                    }

                    return morphedNode;
                } else {
                    // Text node to something else
                    morphedNode = toNode;
                }
            }
        }

        if (morphedNode === toNode) {
            // The "to node" was not compatible with the "from node" so we had to
            // toss out the "from node" and use the "to node"
            onNodeDiscarded(fromNode);
        } else {
            morphEl(morphedNode, toNode, childrenOnly);

            // We now need to loop over any keyed nodes that might need to be
            // removed. We only do the removal if we know that the keyed node
            // never found a match. When a keyed node is matched up we remove
            // it out of fromNodesLookup and we use fromNodesLookup to determine
            // if a keyed node has been matched up or not
            if (keyedRemovalList) {
                for (var i=0, len=keyedRemovalList.length; i<len; i++) {
                    var elToRemove = fromNodesLookup[keyedRemovalList[i]];
                    if (elToRemove) {
                        removeNode(elToRemove, elToRemove.parentNode, false);
                    }
                }
            }
        }

        if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
            if (morphedNode.actualize) {
                morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
            }
            // If we had to swap out the from node with a new node because the old
            // node was not compatible with the target node then we need to
            // replace the old DOM node in the original DOM tree. This is only
            // possible if the original DOM node was part of a DOM tree which
            // we know is the case if it has a parent node.
            fromNode.parentNode.replaceChild(morphedNode, fromNode);
        }

        return morphedNode;
    };
}
<commit_msg>Fix id key removal from tree when the element with key is inside a document fragment node (ex: shadow dom) (#119)<commit_after>'use strict';

import { compareNodeNames, toElement, moveChildren, createElementNS, doc } from './util';
import specialElHandlers from './specialElHandlers';

var ELEMENT_NODE = 1;
var DOCUMENT_FRAGMENT_NODE = 11;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;

function noop() {}

function defaultGetNodeKey(node) {
    return node.id;
}

export default function morphdomFactory(morphAttrs) {

    return function morphdom(fromNode, toNode, options) {
        if (!options) {
            options = {};
        }

        if (typeof toNode === 'string') {
            if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') {
                var toNodeHtml = toNode;
                toNode = doc.createElement('html');
                toNode.innerHTML = toNodeHtml;
            } else {
                toNode = toElement(toNode);
            }
        }

        var getNodeKey = options.getNodeKey || defaultGetNodeKey;
        var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
        var onNodeAdded = options.onNodeAdded || noop;
        var onBeforeElUpdated = options.onBeforeElUpdated || noop;
        var onElUpdated = options.onElUpdated || noop;
        var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
        var onNodeDiscarded = options.onNodeDiscarded || noop;
        var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
        var childrenOnly = options.childrenOnly === true;

        // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
        var fromNodesLookup = {};
        var keyedRemovalList;

        function addKeyedRemoval(key) {
            if (keyedRemovalList) {
                keyedRemovalList.push(key);
            } else {
                keyedRemovalList = [key];
            }
        }

        function walkDiscardedChildNodes(node, skipKeyedNodes) {
            if (node.nodeType === ELEMENT_NODE) {
                var curChild = node.firstChild;
                while (curChild) {

                    var key = undefined;

                    if (skipKeyedNodes && (key = getNodeKey(curChild))) {
                        // If we are skipping keyed nodes then we add the key
                        // to a list so that it can be handled at the very end.
                        addKeyedRemoval(key);
                    } else {
                        // Only report the node as discarded if it is not keyed. We do this because
                        // at the end we loop through all keyed elements that were unmatched
                        // and then discard them in one final pass.
                        onNodeDiscarded(curChild);
                        if (curChild.firstChild) {
                            walkDiscardedChildNodes(curChild, skipKeyedNodes);
                        }
                    }

                    curChild = curChild.nextSibling;
                }
            }
        }

        /**
         * Removes a DOM node out of the original DOM
         *
         * @param  {Node} node The node to remove
         * @param  {Node} parentNode The nodes parent
         * @param  {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
         * @return {undefined}
         */
        function removeNode(node, parentNode, skipKeyedNodes) {
            if (onBeforeNodeDiscarded(node) === false) {
                return;
            }

            if (parentNode) {
                parentNode.removeChild(node);
            }

            onNodeDiscarded(node);
            walkDiscardedChildNodes(node, skipKeyedNodes);
        }

        // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
        // function indexTree(root) {
        //     var treeWalker = document.createTreeWalker(
        //         root,
        //         NodeFilter.SHOW_ELEMENT);
        //
        //     var el;
        //     while((el = treeWalker.nextNode())) {
        //         var key = getNodeKey(el);
        //         if (key) {
        //             fromNodesLookup[key] = el;
        //         }
        //     }
        // }

        // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
        //
        // function indexTree(node) {
        //     var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
        //     var el;
        //     while((el = nodeIterator.nextNode())) {
        //         var key = getNodeKey(el);
        //         if (key) {
        //             fromNodesLookup[key] = el;
        //         }
        //     }
        // }

        function indexTree(node) {
            if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE) {
                var curChild = node.firstChild;
                while (curChild) {
                    var key = getNodeKey(curChild);
                    if (key) {
                        fromNodesLookup[key] = curChild;
                    }

                    // Walk recursively
                    indexTree(curChild);

                    curChild = curChild.nextSibling;
                }
            }
        }

        indexTree(fromNode);

        function handleNodeAdded(el) {
            onNodeAdded(el);

            var curChild = el.firstChild;
            while (curChild) {
                var nextSibling = curChild.nextSibling;

                var key = getNodeKey(curChild);
                if (key) {
                    var unmatchedFromEl = fromNodesLookup[key];
                    if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
                        curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
                        morphEl(unmatchedFromEl, curChild);
                    }
                }

                handleNodeAdded(curChild);
                curChild = nextSibling;
            }
        }

        function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
            // We have processed all of the "to nodes". If curFromNodeChild is
            // non-null then we still have some from nodes left over that need
            // to be removed
            while (curFromNodeChild) {
                var fromNextSibling = curFromNodeChild.nextSibling;
                if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
                    // Since the node is keyed it might be matched up later so we defer
                    // the actual removal to later
                    addKeyedRemoval(curFromNodeKey);
                } else {
                    // NOTE: we skip nested keyed nodes from being removed since there is
                    //       still a chance they will be matched up later
                    removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                }
                curFromNodeChild = fromNextSibling;
            }
        }

        function morphEl(fromEl, toEl, childrenOnly) {
            var toElKey = getNodeKey(toEl);

            if (toElKey) {
                // If an element with an ID is being morphed then it will be in the final
                // DOM so clear it out of the saved elements collection
                delete fromNodesLookup[toElKey];
            }

            if (toNode.isSameNode && toNode.isSameNode(fromNode)) {
                return;
            }

            if (!childrenOnly) {
                // optional
                if (onBeforeElUpdated(fromEl, toEl) === false) {
                    return;
                }

                // update attributes on original DOM element first
                morphAttrs(fromEl, toEl);
                // optional
                onElUpdated(fromEl);

                if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
                    return;
                }
            }
            if (fromEl.nodeName !== 'TEXTAREA') {
              morphChildren(fromEl, toEl);
            } else {
              specialElHandlers.TEXTAREA(fromEl, toEl);
            }
        }

        function morphChildren(fromEl, toEl) {
            var curToNodeChild = toEl.firstChild;
            var curFromNodeChild = fromEl.firstChild;
            var curToNodeKey;
            var curFromNodeKey;

            var fromNextSibling;
            var toNextSibling;
            var matchingFromEl;

            // walk the children
            outer: while (curToNodeChild) {
                toNextSibling = curToNodeChild.nextSibling;
                curToNodeKey = getNodeKey(curToNodeChild);

                // walk the fromNode children all the way through
                while (curFromNodeChild) {
                    fromNextSibling = curFromNodeChild.nextSibling;

                    if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
                        curToNodeChild = toNextSibling;
                        curFromNodeChild = fromNextSibling;
                        continue outer;
                    }

                    curFromNodeKey = getNodeKey(curFromNodeChild);

                    var curFromNodeType = curFromNodeChild.nodeType;

                    // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
                    var isCompatible = undefined;

                    if (curFromNodeType === curToNodeChild.nodeType) {
                        if (curFromNodeType === ELEMENT_NODE) {
                            // Both nodes being compared are Element nodes

                            if (curToNodeKey) {
                                // The target node has a key so we want to match it up with the correct element
                                // in the original DOM tree
                                if (curToNodeKey !== curFromNodeKey) {
                                    // The current element in the original DOM tree does not have a matching key so
                                    // let's check our lookup to see if there is a matching element in the original
                                    // DOM tree
                                    if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
                                        if (fromNextSibling === matchingFromEl) {
                                            // Special case for single element removals. To avoid removing the original
                                            // DOM node out of the tree (since that can break CSS transitions, etc.),
                                            // we will instead discard the current node and wait until the next
                                            // iteration to properly match up the keyed target element with its matching
                                            // element in the original tree
                                            isCompatible = false;
                                        } else {
                                            // We found a matching keyed element somewhere in the original DOM tree.
                                            // Let's move the original DOM node into the current position and morph
                                            // it.

                                            // NOTE: We use insertBefore instead of replaceChild because we want to go through
                                            // the `removeNode()` function for the node that is being discarded so that
                                            // all lifecycle hooks are correctly invoked
                                            fromEl.insertBefore(matchingFromEl, curFromNodeChild);

                                            // fromNextSibling = curFromNodeChild.nextSibling;

                                            if (curFromNodeKey) {
                                                // Since the node is keyed it might be matched up later so we defer
                                                // the actual removal to later
                                                addKeyedRemoval(curFromNodeKey);
                                            } else {
                                                // NOTE: we skip nested keyed nodes from being removed since there is
                                                //       still a chance they will be matched up later
                                                removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                                            }

                                            curFromNodeChild = matchingFromEl;
                                        }
                                    } else {
                                        // The nodes are not compatible since the "to" node has a key and there
                                        // is no matching keyed node in the source tree
                                        isCompatible = false;
                                    }
                                }
                            } else if (curFromNodeKey) {
                                // The original has a key
                                isCompatible = false;
                            }

                            isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
                            if (isCompatible) {
                                // We found compatible DOM elements so transform
                                // the current "from" node to match the current
                                // target DOM node.
                                // MORPH
                                morphEl(curFromNodeChild, curToNodeChild);
                            }

                        } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
                            // Both nodes being compared are Text or Comment nodes
                            isCompatible = true;
                            // Simply update nodeValue on the original node to
                            // change the text value
                            if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
                                curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
                            }

                        }
                    }

                    if (isCompatible) {
                        // Advance both the "to" child and the "from" child since we found a match
                        // Nothing else to do as we already recursively called morphChildren above
                        curToNodeChild = toNextSibling;
                        curFromNodeChild = fromNextSibling;
                        continue outer;
                    }

                    // No compatible match so remove the old node from the DOM and continue trying to find a
                    // match in the original DOM. However, we only do this if the from node is not keyed
                    // since it is possible that a keyed node might match up with a node somewhere else in the
                    // target tree and we don't want to discard it just yet since it still might find a
                    // home in the final DOM tree. After everything is done we will remove any keyed nodes
                    // that didn't find a home
                    if (curFromNodeKey) {
                        // Since the node is keyed it might be matched up later so we defer
                        // the actual removal to later
                        addKeyedRemoval(curFromNodeKey);
                    } else {
                        // NOTE: we skip nested keyed nodes from being removed since there is
                        //       still a chance they will be matched up later
                        removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                    }

                    curFromNodeChild = fromNextSibling;
                } // END: while(curFromNodeChild) {}

                // If we got this far then we did not find a candidate match for
                // our "to node" and we exhausted all of the children "from"
                // nodes. Therefore, we will just append the current "to" node
                // to the end
                if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
                    fromEl.appendChild(matchingFromEl);
                    // MORPH
                    morphEl(matchingFromEl, curToNodeChild);
                } else {
                    var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
                    if (onBeforeNodeAddedResult !== false) {
                        if (onBeforeNodeAddedResult) {
                            curToNodeChild = onBeforeNodeAddedResult;
                        }

                        if (curToNodeChild.actualize) {
                            curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
                        }
                        fromEl.appendChild(curToNodeChild);
                        handleNodeAdded(curToNodeChild);
                    }
                }

                curToNodeChild = toNextSibling;
                curFromNodeChild = fromNextSibling;
            }

            cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);

            var specialElHandler = specialElHandlers[fromEl.nodeName];
            if (specialElHandler) {
                specialElHandler(fromEl, toEl);
            }
        } // END: morphChildren(...)

        var morphedNode = fromNode;
        var morphedNodeType = morphedNode.nodeType;
        var toNodeType = toNode.nodeType;

        if (!childrenOnly) {
            // Handle the case where we are given two DOM nodes that are not
            // compatible (e.g. <div> --> <span> or <div> --> TEXT)
            if (morphedNodeType === ELEMENT_NODE) {
                if (toNodeType === ELEMENT_NODE) {
                    if (!compareNodeNames(fromNode, toNode)) {
                        onNodeDiscarded(fromNode);
                        morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
                    }
                } else {
                    // Going from an element node to a text node
                    morphedNode = toNode;
                }
            } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
                if (toNodeType === morphedNodeType) {
                    if (morphedNode.nodeValue !== toNode.nodeValue) {
                        morphedNode.nodeValue = toNode.nodeValue;
                    }

                    return morphedNode;
                } else {
                    // Text node to something else
                    morphedNode = toNode;
                }
            }
        }

        if (morphedNode === toNode) {
            // The "to node" was not compatible with the "from node" so we had to
            // toss out the "from node" and use the "to node"
            onNodeDiscarded(fromNode);
        } else {
            morphEl(morphedNode, toNode, childrenOnly);

            // We now need to loop over any keyed nodes that might need to be
            // removed. We only do the removal if we know that the keyed node
            // never found a match. When a keyed node is matched up we remove
            // it out of fromNodesLookup and we use fromNodesLookup to determine
            // if a keyed node has been matched up or not
            if (keyedRemovalList) {
                for (var i=0, len=keyedRemovalList.length; i<len; i++) {
                    var elToRemove = fromNodesLookup[keyedRemovalList[i]];
                    if (elToRemove) {
                        removeNode(elToRemove, elToRemove.parentNode, false);
                    }
                }
            }
        }

        if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
            if (morphedNode.actualize) {
                morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
            }
            // If we had to swap out the from node with a new node because the old
            // node was not compatible with the target node then we need to
            // replace the old DOM node in the original DOM tree. This is only
            // possible if the original DOM node was part of a DOM tree which
            // we know is the case if it has a parent node.
            fromNode.parentNode.replaceChild(morphedNode, fromNode);
        }

        return morphedNode;
    };
}
<|endoftext|>